X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=646159a320bfeaf68ac8bc4a59c1e24f2dfd7538;hb=90e29a7415fdbb87d6df022c0115566d52373d2a;hp=8fe8003da8d48fba38289371de0291129729de55;hpb=3c1d225b3097891e863631bdb3b81a1250a5cce4;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 8fe8003..646159a 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, @@ -95,9 +96,7 @@ Dygraph.DEFAULT_ATTRS = { labelsKMG2: false, showLabelsOnHighlight: true, - yValueFormatter: function(x, opt_numDigits) { - return x.toPrecision(opt_numDigits || 2); - }, + yValueFormatter: function(x) { return Dygraph.round_(x, 2); }, strokeWidth: 1.0, @@ -129,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. @@ -160,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 @@ -193,7 +194,10 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.wilsonInterval_ = attrs.wilsonInterval || true; this.is_initial_draw_ = true; this.annotations_ = []; - this.numDigits_ = 2; + + // Zoomed indicators - These indicate when the graph has been zoomed and on what axis. + this.zoomed_x_ = false; + this.zoomed_y_ = false; // Clear the div. This ensure that, if multiple dygraphs are passed the same // div, then only one will be drawn. @@ -257,6 +261,14 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.start_(); }; +// axis is an optional parameter. Can be set to 'x' or 'y'. +Dygraph.prototype.isZoomed = function(axis) { + if (axis == null) return this.zoomed_x_ || this.zoomed_y_; + if (axis == 'x') return this.zoomed_x_; + if (axis == 'y') return this.zoomed_y_; + throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'."; +}; + Dygraph.prototype.attr_ = function(name, seriesName) { if (seriesName && typeof(this.user_attrs_[seriesName]) != 'undefined' && @@ -303,7 +315,7 @@ Dygraph.prototype.error = function(message) { /** * Returns the current rolling period, as set by the user or an option. - * @return {Number} The number of points in the rolling window + * @return {Number} The number of days in the rolling window */ Dygraph.prototype.rollPeriod = function() { return this.rollPeriod_; @@ -433,6 +445,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 @@ -765,242 +794,330 @@ 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; - - // 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.dragGetY_ = function(e, context) { + return Dygraph.pageY(e) - context.py +}; - // 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 ]; - } - } +// 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|. + axis.dragValueRange = yRange[1] - yRange[0]; + axis.initialTopValue = yRange[1]; + axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1); + 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); - self.drawGraph_(); - } - }); + var minDate = context.initialLeftmostDate - + (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel; + var maxDate = minDate + context.dateRange; + g.dateWindow_ = [minDate, maxDate]; - // 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; + // 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 maxValue = axis.initialTopValue + + (context.dragEndY - context.dragStartY) * axis.unitsPerPixel; + 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) { + // 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"); + + // 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])); + } - // Double-clicking zooms back out - Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) { - // Disable zooming out if panning. - if (event.altKey || event.shiftKey) return; + // 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; + } + } }); }; @@ -1087,6 +1204,7 @@ Dygraph.prototype.doZoomX_ = function(lowX, highX) { */ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { this.dateWindow_ = [minDate, maxDate]; + this.zoomed_x_ = true; this.drawGraph_(); if (this.attr_("zoomCallback")) { this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); @@ -1114,9 +1232,11 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) { valueRanges.push([low[1], hi[1]]); } + this.zoomed_y_ = true; this.drawGraph_(); if (this.attr_("zoomCallback")) { var xRange = this.xAxisRange(); + var yRange = this.yAxisRange(); this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges()); } }; @@ -1144,6 +1264,8 @@ Dygraph.prototype.doUnzoom_ = function() { if (dirty) { // Putting the drawing operation before the callback because it resets // yAxisRange. + this.zoomed_x_ = false; + this.zoomed_y_ = false; this.drawGraph_(); if (this.attr_("zoomCallback")) { var minDate = this.rawData_[0][0]; @@ -1174,7 +1296,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; @@ -1284,7 +1406,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); replace += " " + point.name + ":" + yval; @@ -1428,7 +1550,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(); @@ -1465,6 +1589,18 @@ Dygraph.dateString_ = function(date, self) { }; /** + * Round a number to the specified number of digits past the decimal point. + * @param {Number} num The number to round + * @param {Number} places The number of decimals to which to round + * @return {Number} The rounded number + * @private + */ +Dygraph.round_ = function(num, places) { + var shift = Math.pow(10, places); + return Math.round(num * shift)/shift; +}; + +/** * Fires when there's data available to be graphed. * @param {String} data Raw CSV data to be plotted * @private @@ -1493,12 +1629,8 @@ Dygraph.prototype.addXTicks_ = function() { endDate = 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 xTicks = this.attr_('xTicker')(startDate, endDate, this); + this.layout_.updateOptions({xTicks: xTicks}); }; // Time granularity enumeration @@ -1522,7 +1654,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 +1691,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 +1764,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(); @@ -1675,43 +1814,6 @@ Dygraph.dateTicker = function(startDate, endDate, self) { }; /** - * 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 - * significant or not, so by convention we return 1 for all of the following - * inputs: 1, 1.0, 1.00, 1.000 etc. - * @param {Number} x The input value. - * @param {Number} opt_maxPrecision Optional maximum precision to consider. - * Default and maximum allowed value is 13. - * @return {Number} The number of significant figures which is >= 1. - */ -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, - // 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. - var s = x.toExponential(precision); - var ePos = s.lastIndexOf('e'); // -1 case handled by return below. - - for (var i = ePos - 1; i >= 0; i--) { - if (s[i] == '.') { - // Got to the decimal place. We'll call this 1 digit of precision because - // we can't know for sure how many trailing 0s are significant. - return 1; - } else if (s[i] != '0') { - // Found the first non-zero digit. Return the number of characters - // except for the '.'. - return i; // This is i - 1 + 1 (-1 is for '.', +1 is for 0 based index). - } - } - - // Occurs if toExponential() doesn't return a string containing 'e', which - // should never happen. - return 1; -}; - -/** * 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) @@ -1729,7 +1831,7 @@ 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: @@ -1768,7 +1870,7 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { if (low_val > high_val) scale *= -1; for (var i = 0; i < nTicks; i++) { var tickV = low_val + i * scale; - ticks[i] = {v: tickV}; + ticks.push( {v: tickV} ); } } @@ -1784,36 +1886,30 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { k = 1024; k_labels = [ "k", "M", "G", "T" ]; } - var formatter = attr('yAxisLabelFormatter') ? - attr('yAxisLabelFormatter') : attr('yValueFormatter'); - - // Determine the number of decimal places needed for the labels below by - // taking the maximum number of significant figures for any label. We must - // 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); - } + var formatter = attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter'); for (var i = 0; i < ticks.length; i++) { var tickV = ticks[i].v; var absTickV = Math.abs(tickV); - var label = (formatter !== undefined) ? - formatter(tickV, numDigits) : tickV.toPrecision(numDigits); - if (k_labels.length > 0) { + var label; + if (formatter != undefined) { + label = formatter(tickV); + } else { + label = Dygraph.round_(tickV, 2); + } + if (k_labels.length) { // Round up to an appropriate unit. 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 = Dygraph.round_(tickV / n, 1) + k_labels[j]; break; } } } ticks[i].label = label; } - return {ticks: ticks, numDigits: numDigits}; + return ticks; }; // Computes the range of the data series (including confidence intervals). @@ -2004,15 +2100,22 @@ Dygraph.prototype.drawGraph_ = function() { this.layout_.addDataset(this.attr_("labels")[i], datasets[i]); } - this.computeYAxisRanges_(extremes); - this.layout_.updateOptions( { yAxes: this.axes_, - seriesToAxisMap: this.seriesToAxisMap_ - } ); - + if (datasets.length > 0) { + // TODO(danvk): this method doesn't need to return anything. + var out = this.computeYAxisRanges_(extremes); + var axes = out[0]; + var seriesToAxisMap = out[1]; + this.layout_.updateOptions( { yAxes: axes, + seriesToAxisMap: seriesToAxisMap + } ); + } this.addXTicks_(); + // Save the X axis zoomed status as the updateOptions call will tend to set it errorneously + var tmp_zoomed_x = this.zoomed_x_; // Tell PlotKit to use this new data and render itself this.layout_.updateOptions({dateWindow: this.dateWindow_}); + this.zoomed_x_ = tmp_zoomed_x; this.layout_.evaluateWithError(); this.plotter_.clear(); this.plotter_.render(); @@ -2035,7 +2138,16 @@ Dygraph.prototype.drawGraph_ = function() { * indices are into the axes_ array. */ Dygraph.prototype.computeYAxes_ = function() { - this.axes_ = [{}]; // always have at least one y-axis. + var valueWindows; + if (this.axes_ != undefined) { + // Preserve valueWindow settings. + valueWindows = []; + for (var index = 0; index < this.axes_.length; index++) { + valueWindows.push(this.axes_[index].valueWindow); + } + } + + this.axes_ = [{ yAxisId: 0 }]; // always have at least one y-axis. this.seriesToAxisMap_ = {}; // Get a list of series names. @@ -2075,9 +2187,11 @@ 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; Dygraph.update(opts, axis); this.axes_.push(opts); - this.seriesToAxisMap_[seriesName] = this.axes_.length - 1; + this.seriesToAxisMap_[seriesName] = yAxisId; } } @@ -2107,6 +2221,13 @@ Dygraph.prototype.computeYAxes_ = function() { 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]; + } + } }; /** @@ -2185,13 +2306,11 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // primary axis. However, if an axis is specifically marked as having // independent ticks, then that is permissible as well. if (i == 0 || axis.independentTicks) { - var ret = + axis.ticks = Dygraph.numericTicks(axis.computedValueRange[0], axis.computedValueRange[1], this, axis); - axis.ticks = ret.ticks; - this.numDigits_ = ret.numDigits; } else { var p_axis = this.axes_[0]; var p_ticks = p_axis.ticks; @@ -2204,14 +2323,14 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { tick_values.push(y_val); } - var ret = + axis.ticks = Dygraph.numericTicks(axis.computedValueRange[0], axis.computedValueRange[1], this, axis, tick_values); - axis.ticks = ret.ticks; - this.numDigits_ = ret.numDigits; } } + + return [this.axes_, this.seriesToAxisMap_]; }; /** @@ -2223,8 +2342,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { * Note that this is where fractional input (i.e. '5/10') is converted into * decimal values. * @param {Array} originalData The data in the appropriate format (see above) - * @param {Number} rollPeriod The number of points over which to average the - * data + * @param {Number} rollPeriod The number of days over which to average the data */ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { if (originalData.length < 2) @@ -2301,7 +2419,7 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { } } else { // Calculate the rolling average for the first rollPeriod - 1 points where - // there is not enough data to roll over the full number of points + // there is not enough data to roll over the full number of days var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2); if (!this.attr_("errorBars")){ if (rollPeriod == 1) { @@ -2805,6 +2923,12 @@ Dygraph.prototype.updateOptions = function(attrs) { } if ('dateWindow' in attrs) { this.dateWindow_ = attrs.dateWindow; + if (!('noZoomFlagChange' in attrs)) { + this.zoomed_x_ = attrs.dateWindow != null; + } + } + if ('valueRange' in attrs && !('noZoomFlagChange' in attrs)) { + this.zoomed_y_ = attrs.valueRange != null; } // TODO(danvk): validate per-series options. @@ -2873,9 +2997,9 @@ Dygraph.prototype.resize = function(width, height) { }; /** - * Adjusts the number of points in the rolling average. Updates the graph to + * Adjusts the number of days in the rolling average. Updates the graph to * reflect the new averaging period. - * @param {Number} length Number of points over which to average the data. + * @param {Number} length Number of days over which to average the data. */ Dygraph.prototype.adjustRoll = function(length) { this.rollPeriod_ = length;