X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=f5e19ab63afa6644f8759cfe144614c5cf8772b0;hb=e863a17dc17dd729415d3d9fe6c608478a759a4c;hp=8fe8003da8d48fba38289371de0291129729de55;hpb=3c1d225b3097891e863631bdb3b81a1250a5cce4;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 8fe8003..f5e19ab 100644 --- a/dygraph.js +++ b/dygraph.js @@ -24,7 +24,6 @@ If the 'errorBars' option is set in the constructor, the input should be of the form - Date,SeriesA,SeriesB,... YYYYMMDD,A1,sigmaA1,B1,sigmaB1,... YYYYMMDD,A2,sigmaA2,B2,sigmaB2,... @@ -73,12 +72,71 @@ Dygraph.toString = function() { return this.__repr__(); }; +/** + * Formatting to use for an integer number. + * + * @param {Number} x The number to format + * @param {Number} unused_precision The precision to use, ignored. + * @return {String} A string formatted like %g in printf. The max generated + * string length should be precision + 6 (e.g 1.123e+300). + */ +Dygraph.intFormat = function(x, unused_precision) { + return x.toString(); +} + +/** + * Number formatting function which mimicks the behavior of %g in printf, i.e. + * either exponential or fixed format (without trailing 0s) is used depending on + * the length of the generated string. The advantage of this format is that + * there is a predictable upper bound on the resulting string length, + * significant figures are not dropped, and normal numbers are not displayed in + * exponential notation. + * + * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g. + * It creates strings which are too long for absolute values between 10^-4 and + * 10^-6. See tests/number-format.html for output examples. + * + * @param {Number} x The number to format + * @param {Number} opt_precision The precision to use, default 2. + * @return {String} A string formatted like %g in printf. The max generated + * string length should be precision + 6 (e.g 1.123e+300). + */ +Dygraph.floatFormat = function(x, opt_precision) { + // Avoid invalid precision values; [1, 21] is the valid range. + var p = Math.min(Math.max(1, opt_precision || 2), 21); + + // This is deceptively simple. The actual algorithm comes from: + // + // Max allowed length = p + 4 + // where 4 comes from 'e+n' and '.'. + // + // Length of fixed format = 2 + y + p + // where 2 comes from '0.' and y = # of leading zeroes. + // + // Equating the two and solving for y yields y = 2, or 0.00xxxx which is + // 1.0e-3. + // + // Since the behavior of toPrecision() is identical for larger numbers, we + // don't have to worry about the other bound. + // + // Finally, the argument for toExponential() is the number of trailing digits, + // so we take off 1 for the value before the '.'. + return (Math.abs(x) < 1.0e-3 && x != 0.0) ? + x.toExponential(p - 1) : x.toPrecision(p); +}; + // Various default values Dygraph.DEFAULT_ROLL_PERIOD = 1; Dygraph.DEFAULT_WIDTH = 480; Dygraph.DEFAULT_HEIGHT = 320; Dygraph.AXIS_LINE_WIDTH = 0.3; +Dygraph.LOG_SCALE = 10; +Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE); +Dygraph.log10 = function(x) { + return Math.log(x) / Dygraph.LN_TEN; +} + // Default attribute values. Dygraph.DEFAULT_ATTRS = { highlightCircleSize: 3, @@ -95,8 +153,10 @@ Dygraph.DEFAULT_ATTRS = { labelsKMG2: false, showLabelsOnHighlight: true, - yValueFormatter: function(x, opt_numDigits) { - return x.toPrecision(opt_numDigits || 2); + yValueFormatter: function(x, opt_precision) { + var s = Dygraph.floatFormat(x, opt_precision); + var s2 = Dygraph.intFormat(x); + return s.length < s2.length ? s : s2; }, strokeWidth: 1.0, @@ -115,7 +175,6 @@ Dygraph.DEFAULT_ATTRS = { delimiter: ',', - logScale: false, sigma: 2.0, errorBars: false, fractions: false, @@ -129,7 +188,9 @@ Dygraph.DEFAULT_ATTRS = { hideOverlayOnMouseOut: true, stepPlot: false, - avoidMinZero: false + avoidMinZero: false, + + interactionModel: null // will be set to Dygraph.defaultInteractionModel. }; // Various logging levels. @@ -160,7 +221,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 @@ -193,7 +254,20 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.wilsonInterval_ = attrs.wilsonInterval || true; this.is_initial_draw_ = true; this.annotations_ = []; - this.numDigits_ = 2; + + // Number of digits to use when labeling the x (if numeric) and y axis + // ticks. + this.numXDigits_ = 2; + this.numYDigits_ = 2; + + // When labeling x (if numeric) or y values in the legend, there are + // numDigits + numExtraDigits of precision used. For axes labels with N + // digits of precision, the data should be displayed with at least N+1 digits + // of precision. The reason for this is to divide each interval between + // successive ticks into tenths (for 1) or hundredths (for 2), etc. For + // example, if the labels are [0, 1, 2], we want data to be displayed as + // 0.1, 1.3, etc. + this.numExtraDigits_ = 1; // Clear the div. This ensure that, if multiple dygraphs are passed the same // div, then only one will be drawn. @@ -356,44 +430,152 @@ Dygraph.prototype.yAxisRanges = function() { * If specified, do this conversion for the coordinate system of a particular * axis. Uses the first axis by default. * Returns a two-element array: [X, Y] + * + * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord + * instead of toDomCoords(null, y, axis). */ Dygraph.prototype.toDomCoords = function(x, y, axis) { - var ret = [null, null]; + return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ]; +}; + +/** + * Convert from data x coordinates to canvas/div X coordinate. + * If specified, do this conversion for the coordinate system of a particular + * axis. + * Returns a single value or null if x is null. + */ +Dygraph.prototype.toDomXCoord = function(x) { + if (x == null) { + return null; + }; + var area = this.plotter_.area; - if (x !== null) { - var xRange = this.xAxisRange(); - ret[0] = area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w; - } + var xRange = this.xAxisRange(); + return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w; +} - if (y !== null) { - var yRange = this.yAxisRange(axis); - ret[1] = area.y + (yRange[1] - y) / (yRange[1] - yRange[0]) * area.h; - } +/** + * Convert from data x coordinates to canvas/div Y coordinate and optional + * axis. Uses the first axis by default. + * + * returns a single value or null if y is null. + */ +Dygraph.prototype.toDomYCoord = function(y, axis) { + var pct = this.toPercentYCoord(y, axis); - return ret; -}; + if (pct == null) { + return null; + } + var area = this.plotter_.area; + return area.y + pct * area.h; +} /** * Convert from canvas/div coords to data coordinates. * If specified, do this conversion for the coordinate system of a particular * axis. Uses the first axis by default. - * Returns a two-element array: [X, Y] + * Returns a two-element array: [X, Y]. + * + * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord + * instead of toDataCoords(null, y, axis). */ Dygraph.prototype.toDataCoords = function(x, y, axis) { - var ret = [null, null]; + return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ]; +}; + +/** + * Convert from canvas/div x coordinate to data coordinate. + * + * If x is null, this returns null. + */ +Dygraph.prototype.toDataXCoord = function(x) { + if (x == null) { + return null; + } + var area = this.plotter_.area; - if (x !== null) { - var xRange = this.xAxisRange(); - ret[0] = xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]); + var xRange = this.xAxisRange(); + return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]); +}; + +/** + * Convert from canvas/div y coord to value. + * + * If y is null, this returns null. + * if axis is null, this uses the first axis. + */ +Dygraph.prototype.toDataYCoord = function(y, axis) { + if (y == null) { + return null; } - if (y !== null) { - var yRange = this.yAxisRange(axis); - ret[1] = yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]); + var area = this.plotter_.area; + var yRange = this.yAxisRange(axis); + + if (typeof(axis) == "undefined") axis = 0; + if (!this.axes_[axis].logscale) { + return yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]); + } else { + // Computing the inverse of toDomCoord. + var pct = (y - area.y) / area.h + + // Computing the inverse of toPercentYCoord. The function was arrived at with + // the following steps: + // + // Original calcuation: + // pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0])); + // + // Move denominator to both sides: + // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y); + // + // subtract logr1, and take the negative value. + // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y); + // + // Swap both sides of the equation, and we can compute the log of the + // return value. Which means we just need to use that as the exponent in + // e^exponent. + // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))); + + var logr1 = Dygraph.log10(yRange[1]); + var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))); + var value = Math.pow(Dygraph.LOG_SCALE, exponent); + return value; + } +}; + +/** + * Converts a y for an axis to a percentage from the top to the + * bottom of the div. + * + * If the coordinate represents a value visible on the canvas, then + * the value will be between 0 and 1, where 0 is the top of the canvas. + * However, this method will return values outside the range, as + * values can fall outside the canvas. + * + * If y is null, this returns null. + * if axis is null, this uses the first axis. + */ +Dygraph.prototype.toPercentYCoord = function(y, axis) { + if (y == null) { + return null; } + if (typeof(axis) == "undefined") axis = 0; - return ret; -}; + var area = this.plotter_.area; + var yRange = this.yAxisRange(axis); + + var pct; + if (!this.axes_[axis].logscale) { + // yrange[1] - y is unit distance from the bottom. + // yrange[1] - yrange[0] is the scale of the range. + // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom. + pct = (yRange[1] - y) / (yRange[1] - yRange[0]); + } else { + var logr1 = Dygraph.log10(yRange[1]); + pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0])); + } + return pct; +} /** * Returns the number of columns (including the independent variable). @@ -433,6 +615,24 @@ 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 @@ -765,245 +965,351 @@ 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; - - // 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; - - // 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; - - // TODO(danvk): update this comment - // The range in second/value units that the viewport encompasses during a - // panning operation. - var dateRange = null; - - // 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 }; +Dygraph.prototype.dragGetX_ = function(e, context) { + return Dygraph.pageX(e) - context.px +}; - // 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 ]; - } - } +Dygraph.prototype.dragGetY_ = function(e, context) { + return Dygraph.pageY(e) - context.py +}; - self.drawGraph_(); +// 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) { + context.isPanning = true; + var xRange = g.xAxisRange(); + context.dateRange = xRange[1] - xRange[0]; + context.initialLeftmostDate = xRange[0]; + context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1); + + // 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); + // TODO(konigsberg): These values should be in |context|. + // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale. + if (axis.logscale) { + axis.initialTopValue = Dygraph.log10(yRange[1]); + axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]); + } else { + axis.initialTopValue = yRange[1]; + axis.dragValueRange = yRange[1] - yRange[0]; } - }); + axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1); - // 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; + // While calculating axes, set 2dpan. + if (axis.valueWindow || axis.valueRange) context.is2DPan = true; + } +}; + +// 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); + + var minDate = context.initialLeftmostDate - + (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel; + 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. + for (var i = 0; i < g.axes_.length; i++) { + var axis = g.axes_[i]; + + var pixelsDragged = context.dragEndY - context.dragStartY; + var unitsDragged = pixelsDragged * axis.unitsPerPixel; + + // In log scale, maxValue and minValue are the logs of those values. + var maxValue = axis.initialTopValue + unitsDragged; + var minValue = maxValue - axis.dragValueRange; + if (axis.logscale) { + axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue), + Math.pow(Dygraph.LOG_SCALE, maxValue) ]; + } else { + 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) { + // TODO(konigsberg): Clear the context data from the axis. + // TODO(konigsberg): mouseup should just delete the + // context object, and mousedown should create a new one. + context.isPanning = false; + context.is2DPan = false; + context.initialLeftmostDate = 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, + + // The value on the left side of the graph when a pan operation starts. + initialLeftmostDate: null, + + // The number of units each pixel spans. (This won't be valid for log + // scales) + xUnitsPerPixel: 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); + }; + }; + + 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; + } - 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; + } + } }); }; + /** * Draw a gray zoom rectangle over the desired area of the canvas. Also clears * up any previous zoom rectangles that were drawn. This could be optimized to @@ -1026,8 +1332,9 @@ Dygraph.prototype.createDragInterface_ = function() { * function. Used to avoid excess redrawing * @private */ -Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY, - prevDirection, prevEndX, prevEndY) { +Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, + endY, prevDirection, prevEndX, + prevEndY) { var ctx = this.canvas_.getContext("2d"); // Clean up from the previous rect if necessary @@ -1069,10 +1376,8 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY Dygraph.prototype.doZoomX_ = function(lowX, highX) { // Find the earliest and latest dates contained in this canvasx range. // Convert the call to date ranges of the raw data. - var r = this.toDataCoords(lowX, null); - var minDate = r[0]; - r = this.toDataCoords(highX, null); - var maxDate = r[0]; + var minDate = this.toDataXCoord(lowX); + var maxDate = this.toDataXCoord(highX); this.doZoomXDates_(minDate, maxDate); }; @@ -1108,10 +1413,10 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) { // coordinates increase as you go up the screen. var valueRanges = []; for (var i = 0; i < this.axes_.length; i++) { - var hi = this.toDataCoords(null, lowY, i); - var low = this.toDataCoords(null, highY, i); - this.axes_[i].valueWindow = [low[1], hi[1]]; - valueRanges.push([low[1], hi[1]]); + var hi = this.toDataYCoord(lowY, i); + var low = this.toDataYCoord(highY, i); + this.axes_[i].valueWindow = [low, hi]; + valueRanges.push([low, hi]); } this.drawGraph_(); @@ -1164,6 +1469,9 @@ Dygraph.prototype.mouseMove_ = function(event) { var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); var points = this.layout_.points; + // This prevents JS errors when mousing over the canvas before data loads. + if (points === 'undefined') return; + var lastx = -1; var lasty = -1; @@ -1174,16 +1482,12 @@ 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; } if (idx >= 0) lastx = points[idx].xval; - // Check that you can really highlight the last day's data - var last = points[points.length-1]; - if (last != null && canvasx > last.canvasx) - lastx = points[points.length-1].xval; // Extract the points we've selected this.selPoints_ = []; @@ -1270,7 +1574,8 @@ Dygraph.prototype.updateSelection_ = function() { var canvasx = this.selPoints_[0].canvasx; // Set the status message to indicate the selected point(s) - var replace = this.attr_('xValueFormatter')(this.lastx_, this) + ":"; + var replace = this.attr_('xValueFormatter')( + this.lastx_, this.numXDigits_ + this.numExtraDigits_) + ":"; var fmtFunc = this.attr_('yValueFormatter'); var clen = this.colors_.length; @@ -1284,7 +1589,7 @@ Dygraph.prototype.updateSelection_ = function() { } var point = this.selPoints_[i]; var c = new RGBColor(this.plotter_.colors[point.name]); - var yval = fmtFunc(point.yval, this.numDigits_ + 1); // In tenths. + var yval = fmtFunc(point.yval, this.numYDigits_ + this.numExtraDigits_); replace += " " + point.name + ":" + yval; @@ -1428,7 +1733,9 @@ Dygraph.hmsString_ = function(date) { * @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(); @@ -1446,7 +1753,7 @@ Dygraph.dateAxisFormatter = function(date, granularity) { * @return {String} A date of the form "YYYY/MM/DD" * @private */ -Dygraph.dateString_ = function(date, self) { +Dygraph.dateString_ = function(date) { var zeropad = Dygraph.zeropad; var d = new Date(date); @@ -1484,21 +1791,26 @@ Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"]; */ Dygraph.prototype.addXTicks_ = function() { // Determine the correct ticks scale on the x-axis: quarterly, monthly, ... - var startDate, endDate; + var range; if (this.dateWindow_) { - startDate = this.dateWindow_[0]; - endDate = this.dateWindow_[1]; + range = [this.dateWindow_[0], this.dateWindow_[1]]; } else { - startDate = this.rawData_[0][0]; - endDate = this.rawData_[this.rawData_.length - 1][0]; + range = [this.rawData_[0][0], this.rawData_[this.rawData_.length - 1][0]]; } - var ret = this.attr_('xTicker')(startDate, endDate, this); - if (ret.ticks !== undefined) { // Used numericTicks()? - this.layout_.updateOptions({xTicks: ret.ticks}); - } else { // Used dateTicker() instead. - this.layout_.updateOptions({xTicks: ret}); + var formatter = this.attr_('xTicker'); + var ret = formatter(range[0], range[1], this); + var xTicks = []; + + if (ret.ticks !== undefined) { + // numericTicks() returns multiple values. + xTicks = ret.ticks; + this.numXDigits_ = ret.numDigits; + } else { + xTicks = ret; } + + this.layout_.updateOptions({xTicks: xTicks}); }; // Time granularity enumeration @@ -1522,7 +1834,8 @@ Dygraph.QUARTERLY = 16; 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; @@ -1558,6 +1871,7 @@ Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) { 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; @@ -1630,6 +1944,11 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) { } 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(); @@ -1674,6 +1993,69 @@ Dygraph.dateTicker = function(startDate, endDate, self) { } }; +// This is a list of human-friendly values at which to show tick marks on a log +// scale. It is k * 10^n, where k=1..9 and n=-39..+39, so: +// ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ... +// NOTE: this assumes that Dygraph.LOG_SCALE = 10. +Dygraph.PREFERRED_LOG_TICK_VALUES = function() { + var vals = []; + for (var power = -39; power <= 39; power++) { + var range = Math.pow(10, power); + for (var mult = 1; mult <= 9; mult++) { + var val = range * mult; + vals.push(val); + } + } + return vals; +}(); + +// val is the value to search for +// arry is the value over which to search +// if abs > 0, find the lowest entry greater than val +// if abs < 0, find the highest entry less than val +// if abs == 0, find the entry that equals val. +// Currently does not work when val is outside the range of arry's values. +Dygraph.binarySearch = function(val, arry, abs, low, high) { + if (low == null || high == null) { + low = 0; + high = arry.length - 1; + } + if (low > high) { + return -1; + } + if (abs == null) { + abs = 0; + } + var validIndex = function(idx) { + return idx >= 0 && idx < arry.length; + } + var mid = parseInt((low + high) / 2); + var element = arry[mid]; + if (element == val) { + return mid; + } + if (element > val) { + if (abs > 0) { + // Accept if element > val, but also if prior element < val. + var idx = mid - 1; + if (validIndex(idx) && arry[idx] < val) { + return mid; + } + } + return Dygraph.binarySearch(val, arry, abs, low, mid - 1); + } + if (element < val) { + if (abs < 0) { + // Accept if element < val, but also if prior element > val. + var idx = mid + 1; + if (validIndex(idx) && arry[idx] > val) { + return mid; + } + } + return Dygraph.binarySearch(val, arry, abs, mid + 1, high); + } +}; + /** * Determine the number of significant figures in a Number up to the specified * precision. Note that there is no way to determine if a trailing '0' is @@ -1687,7 +2069,7 @@ Dygraph.dateTicker = function(startDate, endDate, self) { Dygraph.significantFigures = function(x, opt_maxPrecision) { var precision = Math.max(opt_maxPrecision || 13, 13); - // Convert the number to it's exponential notation form and work backwards, + // Convert the number to its exponential notation form and work backwards, // ignoring the 'e+xx' bit. This may seem like a hack, but doing a loop and // dividing by 10 leads to roundoff errors. By using toExponential(), we let // the JavaScript interpreter handle the low level bits of the Number for us. @@ -1713,8 +2095,10 @@ Dygraph.significantFigures = function(x, opt_maxPrecision) { /** * Add ticks when the x axis has numbers on it (instead of dates) - * @param {Number} startDate Start of the date window (millis since epoch) - * @param {Number} endDate End of the date window (millis since epoch) + * TODO(konigsberg): Update comment. + * + * @param {Number} minV minimum value + * @param {Number} maxV maximum value * @param self * @param {function} attribute accessor function. * @return {Array.} Array of {label, value} tuples. @@ -1729,46 +2113,92 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { var ticks = []; if (vals) { for (var i = 0; i < vals.length; i++) { - ticks[i] = {v: vals[i]}; + ticks.push({v: vals[i]}); } } else { - // Basic idea: - // Try labels every 1, 2, 5, 10, 20, 50, 100, etc. - // Calculate the resulting tick spacing (i.e. this.height_ / nTicks). - // The first spacing greater than pixelsPerYLabel is what we use. - // TODO(danvk): version that works on a log scale. - if (attr("labelsKMG2")) { - var mults = [1, 2, 4, 8]; - } else { - var mults = [1, 2, 5]; + if (axis_props && attr("logscale")) { + var pixelsPerTick = attr('pixelsPerYLabel'); + // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h? + var nTicks = Math.floor(self.height_ / pixelsPerTick); + var minIdx = Dygraph.binarySearch(minV, Dygraph.PREFERRED_LOG_TICK_VALUES, 1); + var maxIdx = Dygraph.binarySearch(maxV, Dygraph.PREFERRED_LOG_TICK_VALUES, -1); + if (minIdx == -1) { + minIdx = 0; + } + if (maxIdx == -1) { + maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1; + } + // Count the number of tick values would appear, if we can get at least + // nTicks / 4 accept them. + var lastDisplayed = null; + if (maxIdx - minIdx >= nTicks / 4) { + var axisId = axis_props.yAxisId; + for (var idx = maxIdx; idx >= minIdx; idx--) { + var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx]; + var domCoord = axis_props.g.toDomYCoord(tickValue, axisId); + var tick = { v: tickValue }; + if (lastDisplayed == null) { + lastDisplayed = { + tickValue : tickValue, + domCoord : domCoord + }; + } else { + if (domCoord - lastDisplayed.domCoord >= pixelsPerTick) { + lastDisplayed = { + tickValue : tickValue, + domCoord : domCoord + }; + } else { + tick.label = ""; + } + } + ticks.push(tick); + } + // Since we went in backwards order. + ticks.reverse(); + } } - var scale, low_val, high_val, nTicks; - // TODO(danvk): make it possible to set this for x- and y-axes independently. - var pixelsPerTick = attr('pixelsPerYLabel'); - for (var i = -10; i < 50; i++) { + + // ticks.length won't be 0 if the log scale function finds values to insert. + if (ticks.length == 0) { + // Basic idea: + // Try labels every 1, 2, 5, 10, 20, 50, 100, etc. + // Calculate the resulting tick spacing (i.e. this.height_ / nTicks). + // The first spacing greater than pixelsPerYLabel is what we use. + // TODO(danvk): version that works on a log scale. if (attr("labelsKMG2")) { - var base_scale = Math.pow(16, i); + var mults = [1, 2, 4, 8]; } else { - var base_scale = Math.pow(10, i); + var mults = [1, 2, 5]; } - for (var j = 0; j < mults.length; j++) { - scale = base_scale * mults[j]; - low_val = Math.floor(minV / scale) * scale; - high_val = Math.ceil(maxV / scale) * scale; - nTicks = Math.abs(high_val - low_val) / scale; - var spacing = self.height_ / nTicks; - // wish I could break out of both loops at once... + var scale, low_val, high_val, nTicks; + // TODO(danvk): make it possible to set this for x- and y-axes independently. + var pixelsPerTick = attr('pixelsPerYLabel'); + for (var i = -10; i < 50; i++) { + if (attr("labelsKMG2")) { + var base_scale = Math.pow(16, i); + } else { + var base_scale = Math.pow(10, i); + } + for (var j = 0; j < mults.length; j++) { + scale = base_scale * mults[j]; + low_val = Math.floor(minV / scale) * scale; + high_val = Math.ceil(maxV / scale) * scale; + nTicks = Math.abs(high_val - low_val) / scale; + var spacing = self.height_ / nTicks; + // wish I could break out of both loops at once... + if (spacing > pixelsPerTick) break; + } if (spacing > pixelsPerTick) break; } - if (spacing > pixelsPerTick) break; - } - // Construct the set of ticks. - // Allow reverse y-axis if it's explicitly requested. - if (low_val > high_val) scale *= -1; - for (var i = 0; i < nTicks; i++) { - var tickV = low_val + i * scale; - ticks[i] = {v: tickV}; + // Construct the set of ticks. + // Allow reverse y-axis if it's explicitly requested. + if (low_val > high_val) scale *= -1; + for (var i = 0; i < nTicks; i++) { + var tickV = low_val + i * scale; + ticks.push( {v: tickV} ); + } } } @@ -1792,11 +2222,12 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { // take the max because we can't tell if trailing 0s are significant. var numDigits = 0; for (var i = 0; i < ticks.length; i++) { - var tickV = ticks[i].v; - numDigits = Math.max(Dygraph.significantFigures(tickV), numDigits); + numDigits = Math.max(Dygraph.significantFigures(ticks[i].v), numDigits); } + // Add labels to the ticks. for (var i = 0; i < ticks.length; i++) { + if (ticks[i].label !== undefined) continue; // Use current label. var tickV = ticks[i].v; var absTickV = Math.abs(tickV); var label = (formatter !== undefined) ? @@ -1806,13 +2237,14 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { var n = k*k*k*k; for (var j = 3; j >= 0; j--, n /= k) { if (absTickV >= n) { - label = (tickV / n).toPrecision(numDigits) + k_labels[j]; + label = formatter(tickV / n, numDigits) + k_labels[j]; break; } } } ticks[i].label = label; } + return {ticks: ticks, numDigits: numDigits}; }; @@ -1887,7 +2319,6 @@ Dygraph.prototype.predraw_ = function() { }; /** -======= * 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. @@ -1919,12 +2350,24 @@ Dygraph.prototype.drawGraph_ = function() { var seriesName = this.attr_("labels")[i]; var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i); + var logScale = this.attr_('logscale', i); var series = []; for (var j = 0; j < data.length; j++) { - if (data[j][i] != null || !connectSeparatedPoints) { - var date = data[j][0]; - series.push([date, data[j][i]]); + var date = data[j][0]; + var point = data[j][i]; + if (logScale) { + // On the log scale, points less than zero do not exist. + // This will create a gap in the chart. Note that this ignores + // connectSeparatedPoints. + if (point <= 0) { + point = null; + } + series.push([date, point]); + } else { + if (point != null || !connectSeparatedPoints) { + series.push([date, point]); + } } } @@ -2035,7 +2478,7 @@ Dygraph.prototype.drawGraph_ = function() { * indices are into the axes_ array. */ Dygraph.prototype.computeYAxes_ = function() { - this.axes_ = [{}]; // always have at least one y-axis. + this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis. this.seriesToAxisMap_ = {}; // Get a list of series names. @@ -2052,7 +2495,8 @@ Dygraph.prototype.computeYAxes_ = function() { 'pixelsPerYLabel', 'yAxisLabelWidth', 'axisLabelFontSize', - 'axisTickSize' + 'axisTickSize', + 'logscale' ]; // Copy global axis options over to the first axis. @@ -2075,9 +2519,12 @@ Dygraph.prototype.computeYAxes_ = function() { var opts = {}; Dygraph.update(opts, this.axes_[0]); Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this. + var yAxisId = this.axes_.length; + opts.yAxisId = yAxisId; + opts.g = this; Dygraph.update(opts, axis); this.axes_.push(opts); - this.seriesToAxisMap_[seriesName] = this.axes_.length - 1; + this.seriesToAxisMap_[seriesName] = yAxisId; } } @@ -2164,18 +2611,26 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { var span = maxY - minY; // special case: if we have no sense of scale, use +/-10% of the sole value. if (span == 0) { span = maxY; } - var maxAxisY = maxY + 0.1 * span; - var minAxisY = minY - 0.1 * span; - // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense. - if (!this.attr_("avoidMinZero")) { - if (minAxisY < 0 && minY >= 0) minAxisY = 0; - if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; - } + var maxAxisY; + var minAxisY; + if (axis.logscale) { + var maxAxisY = maxY + 0.1 * span; + var minAxisY = minY; + } else { + var maxAxisY = maxY + 0.1 * span; + var minAxisY = minY - 0.1 * span; - if (this.attr_("includeZero")) { - if (maxY < 0) maxAxisY = 0; - if (minY > 0) minAxisY = 0; + // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense. + if (!this.attr_("avoidMinZero")) { + if (minAxisY < 0 && minY >= 0) minAxisY = 0; + if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; + } + + if (this.attr_("includeZero")) { + if (maxY < 0) maxAxisY = 0; + if (minY > 0) minAxisY = 0; + } } axis.computedValueRange = [minAxisY, maxAxisY]; @@ -2191,7 +2646,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { this, axis); axis.ticks = ret.ticks; - this.numDigits_ = ret.numDigits; + this.numYDigits_ = ret.numDigits; } else { var p_axis = this.axes_[0]; var p_ticks = p_axis.ticks; @@ -2209,7 +2664,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { axis.computedValueRange[1], this, axis, tick_values); axis.ticks = ret.ticks; - this.numDigits_ = ret.numDigits; + this.numYDigits_ = ret.numDigits; } } }; @@ -2392,7 +2847,7 @@ Dygraph.dateParser = function(dateStr, self) { */ Dygraph.prototype.detectTypeFromString_ = function(str) { var isDate = false; - if (str.indexOf('-') >= 0 || + if (str.indexOf('-') > 0 || str.indexOf('/') >= 0 || isNaN(parseFloat(str))) { isDate = true; @@ -2407,7 +2862,7 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { this.attrs_.xTicker = Dygraph.dateTicker; this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else { - this.attrs_.xValueFormatter = function(x) { return x; }; + this.attrs_.xValueFormatter = this.attrs_.xValueFormatter; this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.xTicker = Dygraph.numericTicks; this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter; @@ -2570,7 +3025,7 @@ Dygraph.prototype.parseArray_ = function(data) { return parsedData; } else { // Some intelligent defaults for a numeric x-axis. - this.attrs_.xValueFormatter = function(x) { return x; }; + this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; this.attrs_.xTicker = Dygraph.numericTicks; return data; } @@ -2596,7 +3051,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { this.attrs_.xTicker = Dygraph.dateTicker; this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else if (indepType == 'number') { - this.attrs_.xValueFormatter = function(x) { return x; }; + this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.xTicker = Dygraph.numericTicks; this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;