X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=9283a183333f7f37597c371185ce6b3b265892b7;hb=5061b42f5e133a17132897f82f59170787ce80b0;hp=c04bccc724284eb6cb88306db3007f681731496c;hpb=48e614acf3084b9836803f8b014bf0e65f1ca119;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index c04bccc..9283a18 100644 --- a/dygraph.js +++ b/dygraph.js @@ -1,5 +1,8 @@ -// Copyright 2006 Dan Vanderkam (danvdk@gmail.com) -// All Rights Reserved. +/** + * @license + * Copyright 2006 Dan Vanderkam (danvdk@gmail.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ /** * @fileoverview Creates an interactive, zoomable graph based on a CSV file or @@ -40,6 +43,8 @@ */ +"use strict"; + /** * Creates an interactive, zoomable chart. * @@ -54,7 +59,7 @@ * whether the input data contains error ranges. For a complete list of * options, see http://dygraphs.com/options.html. */ -Dygraph = function(div, data, opts) { +var Dygraph = function(div, data, opts) { if (arguments.length > 0) { if (arguments.length == 4) { // Old versions of dygraphs took in the series labels as a constructor @@ -86,6 +91,9 @@ Dygraph.DEFAULT_ROLL_PERIOD = 1; Dygraph.DEFAULT_WIDTH = 480; Dygraph.DEFAULT_HEIGHT = 320; +Dygraph.ANIMATION_STEPS = 10; +Dygraph.ANIMATION_DURATION = 200; + // These are defined before DEFAULT_ATTRS so that it can refer to them. /** * @private @@ -240,6 +248,13 @@ Dygraph.DEFAULT_ATTRS = { gridLineColor: "rgb(128,128,128)", interactionModel: null, // will be set to Dygraph.Interaction.defaultModel + animatedZooms: false, // (for now) + + // Range selector options + showRangeSelector: false, + rangeSelectorHeight: 40, + rangeSelectorPlotStrokeColor: "#808FAB", + rangeSelectorPlotFillColor: "#A7B1C4", // per-axis options axes: { @@ -302,6 +317,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { document.readyState != 'complete') { var self = this; setTimeout(function() { self.__init__(div, file, attrs) }, 100); + return; } // Support two-argument constructor @@ -314,6 +330,8 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { return; } + this.isUsingExcanvas_ = typeof(G_vmlCanvasManager) != 'undefined'; + // Copy the important bits into the object // TODO(danvk): most of these should just stay in the attrs_ dictionary. this.maindiv_ = div; @@ -345,15 +363,15 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { if (div.style.height == '' && attrs.height) { div.style.height = attrs.height + "px"; } - if (div.style.height == '' && div.offsetHeight == 0) { + if (div.style.height == '' && div.clientHeight == 0) { div.style.height = Dygraph.DEFAULT_HEIGHT + "px"; if (div.style.width == '') { div.style.width = Dygraph.DEFAULT_WIDTH + "px"; } } // these will be zero if the dygraph's div is hidden. - this.width_ = div.offsetWidth; - this.height_ = div.offsetHeight; + this.width_ = div.clientWidth; + this.height_ = div.clientHeight; // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_. if (attrs['stackedGraph']) { @@ -776,10 +794,33 @@ Dygraph.prototype.createInterface_ = function() { this.hidden_ = this.createPlotKitCanvas_(this.canvas_); this.hidden_ctx_ = Dygraph.getContext(this.hidden_); + if (this.attr_('showRangeSelector')) { + // The range selector must be created here so that its canvases and contexts get created here. + // For some reason, if the canvases and contexts don't get created here, things don't work in IE. + // The range selector also sets xAxisHeight in order to reserve space. + this.rangeSelector_ = new DygraphRangeSelector(this); + } + // The interactive parts of the graph are drawn on top of the chart. this.graphDiv.appendChild(this.hidden_); this.graphDiv.appendChild(this.canvas_); - this.mouseEventElement_ = this.canvas_; + this.mouseEventElement_ = this.createMouseEventElement_(); + + // Create the grapher + this.layout_ = new DygraphLayout(this); + + if (this.rangeSelector_) { + // This needs to happen after the graph canvases are added to the div and the layout object is created. + this.rangeSelector_.addToGraph(this.graphDiv, this.layout_); + } + + // Create the grapher + this.layout_ = new DygraphLayout(this); + + if (this.rangeSelector_) { + // This needs to happen after the graph canvases are added to the div and the layout object is created. + this.rangeSelector_.addToGraph(this.graphDiv, this.layout_); + } var dygraph = this; Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) { @@ -789,9 +830,6 @@ Dygraph.prototype.createInterface_ = function() { dygraph.mouseOut_(e); }); - // Create the grapher - this.layout_ = new DygraphLayout(this); - this.createStatusMessage_(); this.createDragInterface_(); @@ -854,6 +892,26 @@ Dygraph.prototype.createPlotKitCanvas_ = function(canvas) { }; /** + * Creates an overlay element used to handle mouse events. + * @return {Object} The mouse event element. + * @private + */ +Dygraph.prototype.createMouseEventElement_ = function() { + if (this.isUsingExcanvas_) { + var elem = document.createElement("div"); + elem.style.position = 'absolute'; + elem.style.backgroundColor = 'white'; + elem.style.filter = 'alpha(opacity=0)'; + elem.style.width = this.width_ + "px"; + elem.style.height = this.height_ + "px"; + this.graphDiv.appendChild(elem); + return elem; + } else { + return this.canvas_; + } +}; + +/** * Generate a set of distinct colors for the data series. This is done with a * color wheel. Saturation/Value are customizable, and the hue is * equally-spaced around the color wheel. If a custom set of colors is @@ -1097,13 +1155,12 @@ Dygraph.prototype.createDragInterface_ = function() { }); }; - /** * 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 * avoid extra redrawing, but it's tricky to avoid interactions with the status * dots. - * + * * @param {Number} direction the direction of the zoom rectangle. Acceptable * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL. * @param {Number} startX The X position where the drag started, in canvas @@ -1127,28 +1184,40 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, // Clean up from the previous rect if necessary if (prevDirection == Dygraph.HORIZONTAL) { - ctx.clearRect(Math.min(startX, prevEndX), 0, - Math.abs(startX - prevEndX), this.height_); + ctx.clearRect(Math.min(startX, prevEndX), this.layout_.getPlotArea().y, + Math.abs(startX - prevEndX), this.layout_.getPlotArea().h); } else if (prevDirection == Dygraph.VERTICAL){ - ctx.clearRect(0, Math.min(startY, prevEndY), - this.width_, Math.abs(startY - prevEndY)); + ctx.clearRect(this.layout_.getPlotArea().x, Math.min(startY, prevEndY), + this.layout_.getPlotArea().w, Math.abs(startY - prevEndY)); } // Draw a light-grey rectangle to show the new viewing area if (direction == Dygraph.HORIZONTAL) { if (endX && startX) { ctx.fillStyle = "rgba(128,128,128,0.33)"; - ctx.fillRect(Math.min(startX, endX), 0, - Math.abs(endX - startX), this.height_); + ctx.fillRect(Math.min(startX, endX), this.layout_.getPlotArea().y, + Math.abs(endX - startX), this.layout_.getPlotArea().h); } - } - if (direction == Dygraph.VERTICAL) { + } else if (direction == Dygraph.VERTICAL) { if (endY && startY) { ctx.fillStyle = "rgba(128,128,128,0.33)"; - ctx.fillRect(0, Math.min(startY, endY), - this.width_, Math.abs(endY - startY)); + ctx.fillRect(this.layout_.getPlotArea().x, Math.min(startY, endY), + this.layout_.getPlotArea().w, Math.abs(endY - startY)); } } + + if (this.isUsingExcanvas_) { + this.currentZoomRectArgs_ = [direction, startX, endX, startY, endY, 0, 0, 0]; + } +}; + +/** + * Clear the zoom rectangle (and perform no zoom). + * @private + */ +Dygraph.prototype.clearZoomRect_ = function() { + this.currentZoomRectArgs_ = null; + this.canvas_ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.height); }; /** @@ -1162,6 +1231,7 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, * @private */ Dygraph.prototype.doZoomX_ = function(lowX, highX) { + this.currentZoomRectArgs_ = null; // Find the earliest and latest dates contained in this canvasx range. // Convert the call to date ranges of the raw data. var minDate = this.toDataXCoord(lowX); @@ -1170,6 +1240,16 @@ Dygraph.prototype.doZoomX_ = function(lowX, highX) { }; /** + * Transition function to use in animations. Returns values between 0.0 + * (totally old values) and 1.0 (totally new values) for each frame. + * @private + */ +Dygraph.zoomAnimationFunction = function(frame, numFrames) { + var k = 1.5; + return (1.0 - Math.pow(k, -frame)) / (1.0 - Math.pow(k, -numFrames)); +}; + +/** * Zoom to something containing [minDate, maxDate] values. Don't confuse this * method with doZoomX which accepts pixel coordinates. This function redraws * the graph. @@ -1179,12 +1259,18 @@ Dygraph.prototype.doZoomX_ = function(lowX, highX) { * @private */ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { - this.dateWindow_ = [minDate, maxDate]; + // TODO(danvk): when yAxisRange is null (i.e. "fit to data", the animation + // can produce strange effects. Rather than the y-axis transitioning slowly + // between values, it can jerk around.) + var old_window = this.xAxisRange(); + var new_window = [minDate, maxDate]; this.zoomed_x_ = true; - this.drawGraph_(); - if (this.attr_("zoomCallback")) { - this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); - } + var that = this; + this.doAnimatedZoom(old_window, new_window, null, null, function() { + if (that.attr_("zoomCallback")) { + that.attr_("zoomCallback")(minDate, maxDate, that.yAxisRanges()); + } + }); }; /** @@ -1196,25 +1282,28 @@ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { * @private */ Dygraph.prototype.doZoomY_ = function(lowY, highY) { + this.currentZoomRectArgs_ = null; // Find the highest and lowest values in pixel range for each axis. // Note that lowY (in pixels) corresponds to the max Value (in data coords). // This is because pixels increase as you go down on the screen, whereas data // coordinates increase as you go up the screen. - var valueRanges = []; + var oldValueRanges = this.yAxisRanges(); + var newValueRanges = []; for (var i = 0; i < this.axes_.length; i++) { var hi = this.toDataYCoord(lowY, i); var low = this.toDataYCoord(highY, i); - this.axes_[i].valueWindow = [low, hi]; - valueRanges.push([low, hi]); + newValueRanges.push([low, hi]); } 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()); - } + var that = this; + this.doAnimatedZoom(null, null, oldValueRanges, newValueRanges, function() { + if (that.attr_("zoomCallback")) { + var xRange = that.xAxisRange(); + var yRange = that.yAxisRange(); + that.attr_("zoomCallback")(xRange[0], xRange[1], that.yAxisRanges()); + } + }); }; /** @@ -1224,16 +1313,16 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) { * @private */ Dygraph.prototype.doUnzoom_ = function() { - var dirty = false; + var dirty = false, dirtyX = false, dirtyY = false; if (this.dateWindow_ != null) { dirty = true; - this.dateWindow_ = null; + dirtyX = true; } for (var i = 0; i < this.axes_.length; i++) { if (this.axes_[i].valueWindow != null) { dirty = true; - delete this.axes_[i].valueWindow; + dirtyY = true; } } @@ -1241,20 +1330,115 @@ Dygraph.prototype.doUnzoom_ = function() { this.clearSelection(); 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]; - var maxDate = this.rawData_[this.rawData_.length - 1][0]; - this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); + + var minDate = this.rawData_[0][0]; + var maxDate = this.rawData_[this.rawData_.length - 1][0]; + + // With only one frame, don't bother calculating extreme ranges. + // TODO(danvk): merge this block w/ the code below. + if (!this.attr_("animatedZooms")) { + this.dateWindow_ = null; + for (var i = 0; i < this.axes_.length; i++) { + if (this.axes_[i].valueWindow != null) { + delete this.axes_[i].valueWindow; + } + } + this.drawGraph_(); + if (this.attr_("zoomCallback")) { + this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); + } + return; + } + + var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null; + if (dirtyX) { + oldWindow = this.xAxisRange(); + newWindow = [minDate, maxDate]; + } + + if (dirtyY) { + oldValueRanges = this.yAxisRanges(); + // TODO(danvk): this is pretty inefficient + var packed = this.gatherDatasets_(this.rolledSeries_, null); + var extremes = packed[1]; + + // this has the side-effect of modifying this.axes_. + // this doesn't make much sense in this context, but it's convenient (we + // need this.axes_[*].extremeValues) and not harmful since we'll be + // calling drawGraph_ shortly, which clobbers these values. + this.computeYAxisRanges_(extremes); + + newValueRanges = []; + for (var i = 0; i < this.axes_.length; i++) { + newValueRanges.push(this.axes_[i].extremeRange); + } } + + var that = this; + this.doAnimatedZoom(oldWindow, newWindow, oldValueRanges, newValueRanges, + function() { + that.dateWindow_ = null; + for (var i = 0; i < that.axes_.length; i++) { + if (that.axes_[i].valueWindow != null) { + delete that.axes_[i].valueWindow; + } + } + if (that.attr_("zoomCallback")) { + that.attr_("zoomCallback")(minDate, maxDate, that.yAxisRanges()); + } + }); } }; /** + * Combined animation logic for all zoom functions. + * either the x parameters or y parameters may be null. + * @private + */ +Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, newYRanges, callback) { + var steps = this.attr_("animatedZooms") ? Dygraph.ANIMATION_STEPS : 1; + + var windows = []; + var valueRanges = []; + + if (oldXRange != null && newXRange != null) { + for (var step = 1; step <= steps; step++) { + var frac = Dygraph.zoomAnimationFunction(step, steps); + windows[step-1] = [oldXRange[0]*(1-frac) + frac*newXRange[0], + oldXRange[1]*(1-frac) + frac*newXRange[1]]; + } + } + + if (oldYRanges != null && newYRanges != null) { + for (var step = 1; step <= steps; step++) { + var frac = Dygraph.zoomAnimationFunction(step, steps); + var thisRange = []; + for (var j = 0; j < this.axes_.length; j++) { + thisRange.push([oldYRanges[j][0]*(1-frac) + frac*newYRanges[j][0], + oldYRanges[j][1]*(1-frac) + frac*newYRanges[j][1]]); + } + valueRanges[step-1] = thisRange; + } + } + + var that = this; + Dygraph.repeatAndCleanup(function(step) { + if (valueRanges.length) { + for (var i = 0; i < that.axes_.length; i++) { + var w = valueRanges[step][i]; + that.axes_[i].valueWindow = [w[0], w[1]]; + } + } + if (windows.length) { + that.dateWindow_ = windows[step]; + } + that.drawGraph_(); + }, steps, Dygraph.ANIMATION_DURATION / steps, callback); +}; + +/** * When the mouse moves in the canvas, display information about a nearby data * point and draw dots over those points in the data series. This function * takes care of cleanup of previously-drawn dots. @@ -1444,6 +1628,10 @@ Dygraph.prototype.updateSelection_ = function() { 2 * maxCircleSize + 2, this.height_); } + if (this.isUsingExcanvas_ && this.currentZoomRectArgs_) { + Dygraph.prototype.drawZoomRect_.apply(this, this.currentZoomRectArgs_); + } + if (this.selPoints_.length > 0) { // Set the status message to indicate the selected point(s) if (this.attr_('showLabelsOnHighlight')) { @@ -1482,18 +1670,18 @@ Dygraph.prototype.setSelection = function(row) { var pos = 0; if (row !== false) { - row = row-this.boundaryIds_[0][0]; + row = row - this.boundaryIds_[0][0]; } if (row !== false && row >= 0) { for (var i in this.layout_.datasets) { if (row < this.layout_.datasets[i].length) { var point = this.layout_.points[pos+row]; - + if (this.attr_("stackedGraph")) { point = this.layout_.unstackPointAtIndex(pos+row); } - + this.selPoints_.push(point); } pos += this.layout_.datasets[i].length; @@ -1662,6 +1850,21 @@ Dygraph.prototype.predraw_ = function() { // edge of the div, if we have two y-axes. this.positionLabelsDiv_(); + if (this.rangeSelector_) { + this.rangeSelector_.renderStaticLayer(); + } + + // Convert the raw data (a 2D array) into the internal format and compute + // rolling averages. + this.rolledSeries_ = [null]; // x-axis is the first series and it's special + for (var i = 1; i < this.rawData_[0].length; i++) { + var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i); + var logScale = this.attr_('logscale', i); + var series = this.extractSeries_(this.rawData_, i, logScale, connectSeparatedPoints); + series = this.rollingAverage(series, this.rollPeriod_); + this.rolledSeries_.push(series); + } + // If the data or options have changed, then we'd better redraw. this.drawGraph_(); @@ -1671,80 +1874,42 @@ 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. + * Loop over all fields and create datasets, calculating extreme y-values for + * each series and extreme x-indices as we go. * - * 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.) + * dateWindow is passed in as an explicit parameter so that we can compute + * extreme values "speculatively", i.e. without actually setting state on the + * dygraph. * + * TODO(danvk): make this more of a true function + * @return [ datasets, seriesExtremes, boundaryIds ] * @private */ -Dygraph.prototype.drawGraph_ = function(clearSelection) { - var start = new Date(); - - if (typeof(clearSelection) === 'undefined') { - clearSelection = true; - } - - var data = this.rawData_; - - // This is used to set the second parameter to drawCallback, below. - var is_initial_draw = this.is_initial_draw_; - this.is_initial_draw_ = false; - - var minY = null, maxY = null; - this.layout_.removeAllDatasets(); - this.setColors_(); - this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize'); - - // Loop over the fields (series). Go from the last to the first, - // because if they're stacked that's how we accumulate the values. - +Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { + var boundaryIds = []; var cumulative_y = []; // For stacked series. var datasets = []; - var extremes = {}; // series name -> [low, high] - // Loop over all fields and create datasets - for (var i = data[0].length - 1; i >= 1; i--) { + // Loop over the fields (series). Go from the last to the first, + // because if they're stacked that's how we accumulate the values. + var num_series = rolledSeries.length - 1; + for (var i = num_series; i >= 1; i--) { if (!this.visibility()[i - 1]) continue; - var seriesName = this.attr_("labels")[i]; - var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i); - var logScale = this.attr_('logscale', i); - + // TODO(danvk): is this copy really necessary? var series = []; - for (var j = 0; j < data.length; j++) { - 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]); - } - } + for (var j = 0; j < rolledSeries[i].length; j++) { + series.push(rolledSeries[i][j]); } - // TODO(danvk): move this into predraw_. It's insane to do it here. - series = this.rollingAverage(series, this.rollPeriod_); - // Prune down to the desired range, if necessary (for zooming) // Because there can be lines going to points outside of the visible area, // we actually prune to visible points, plus one on either side. var bars = this.attr_("errorBars") || this.attr_("customBars"); - if (this.dateWindow_) { - var low = this.dateWindow_[0]; - var high= this.dateWindow_[1]; + if (dateWindow) { + var low = dateWindow[0]; + var high = dateWindow[1]; var pruned = []; // TODO(danvk): do binary search instead of linear search. // TODO(danvk): pass firstIdx and lastIdx directly to the renderer. @@ -1761,21 +1926,23 @@ Dygraph.prototype.drawGraph_ = function(clearSelection) { if (firstIdx > 0) firstIdx--; if (lastIdx === null) lastIdx = series.length - 1; if (lastIdx < series.length - 1) lastIdx++; - this.boundaryIds_[i-1] = [firstIdx, lastIdx]; + boundaryIds[i-1] = [firstIdx, lastIdx]; for (var k = firstIdx; k <= lastIdx; k++) { pruned.push(series[k]); } series = pruned; } else { - this.boundaryIds_[i-1] = [0, series.length-1]; + boundaryIds[i-1] = [0, series.length-1]; } var seriesExtremes = this.extremeValues_(series); if (bars) { for (var j=0; j 0 || + var dashPos = str.indexOf('-'); // could be 2006-01-01 _or_ 1.0e-2 + if ((dashPos > 0 && (str[dashPos-1] != 'e' && str[dashPos-1] != 'E')) || str.indexOf('/') >= 0 || isNaN(parseFloat(str))) { isDate = true; @@ -2821,8 +3061,8 @@ Dygraph.prototype.resize = function(width, height) { this.width_ = width; this.height_ = height; } else { - this.width_ = this.maindiv_.offsetWidth; - this.height_ = this.maindiv_.offsetHeight; + this.width_ = this.maindiv_.clientWidth; + this.height_ = this.maindiv_.clientHeight; } if (old_width != this.width_ || old_height != this.height_) { @@ -2831,6 +3071,10 @@ Dygraph.prototype.resize = function(width, height) { this.roller_ = null; this.attrs_.labelsDiv = null; this.createInterface_(); + if (this.annotations_.length) { + // createInterface_ reset the layout, so we need to do this. + this.layout_.setAnnotations(this.annotations_); + } this.predraw_(); } @@ -2887,6 +3131,9 @@ Dygraph.prototype.size = function() { /** * Update the list of annotations and redraw the chart. + * See dygraphs.com/annotations.html for more info on how to use annotations. + * @param ann {Array} An array of annotation objects. + * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional). */ Dygraph.prototype.setAnnotations = function(ann, suppressDraw) { // Only add the annotation CSS rule once we know it will be used. @@ -2959,4 +3206,4 @@ Dygraph.addAnnotationRule = function() { } // Older pages may still use this name. -DateGraph = Dygraph; +var DateGraph = Dygraph;