X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=8e06ba2e1f08fa5f96dc0064da636e03f284dac8;hb=4c10c8d21b6858ea9029bdb789f487d9103d72f9;hp=47c6d2c32133cf9fee5af0801e4801d7d7517b17;hpb=6a4587ace91189585976885b489d3e85eb51ee98;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 47c6d2c..8e06ba2 100644 --- a/dygraph.js +++ b/dygraph.js @@ -44,7 +44,7 @@ */ /*jshint globalstrict: true */ -/*global DygraphRangeSelector:false, DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager:false */ +/*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false */ "use strict"; /** @@ -91,7 +91,8 @@ Dygraph.DEFAULT_ROLL_PERIOD = 1; Dygraph.DEFAULT_WIDTH = 480; Dygraph.DEFAULT_HEIGHT = 320; -Dygraph.ANIMATION_STEPS = 10; +// For max 60 Hz. animation: +Dygraph.ANIMATION_STEPS = 12; Dygraph.ANIMATION_DURATION = 200; // These are defined before DEFAULT_ATTRS so that it can refer to them. @@ -180,6 +181,18 @@ Dygraph.dateAxisFormatter = function(date, granularity) { } }; +/** + * Standard plotters. These may be used by clients. + * Available plotters are: + * - Dygraph.Plotters.linePlotter: draws central lines (most common) + * - Dygraph.Plotters.errorPlotter: draws error bars + * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph) + * + * By default, the plotter is [fillPlotter, errorPlotter, linePlotter]. + * This causes all the lines to be drawn over all the fills/error bars. + */ +Dygraph.Plotters = DygraphCanvasRenderer._Plotters; + // Default attribute values. Dygraph.DEFAULT_ATTRS = { @@ -233,6 +246,7 @@ Dygraph.DEFAULT_ATTRS = { stepPlot: false, avoidMinZero: false, + drawAxesAtZero: false, // Sizes of the various chart labels. titleHeight: 28, @@ -260,6 +274,16 @@ Dygraph.DEFAULT_ATTRS = { rangeSelectorPlotStrokeColor: "#808FAB", rangeSelectorPlotFillColor: "#A7B1C4", + // The ordering here ensures that central lines always appear above any + // fill bars/error bars. + plotter: [ + Dygraph.Plotters.fillPlotter, + Dygraph.Plotters.errorPlotter, + Dygraph.Plotters.linePlotter + ], + + plugins: [ ], + // per-axis options axes: { x: { @@ -334,6 +358,10 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { attrs = Dygraph.mapLegacyOptions_(attrs); + if (typeof(div) == 'string') { + div = document.getElementById(div); + } + if (!div) { Dygraph.error("Constructing dygraph with a non-existent div!"); return; @@ -387,6 +415,9 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // TODO(nikhilk): Add any other stackedGraph checks here. } + // DEPRECATION WARNING: All option processing should be moved from + // attrs_ and user_attrs_ to options_, which holds all this information. + // // Dygraphs has many options, some of which interact with one another. // To keep track of everything, we maintain two sets of options: // @@ -408,15 +439,19 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.datasetIndex_ = []; this.registeredEvents_ = []; + this.eventListeners_ = {}; + + this.attributes_ = new DygraphOptions(this); // Create the containing DIV and other interactive elements this.createInterface_(); // Activate plugins. this.plugins_ = []; - for (var i = 0; i < Dygraph.PLUGINS.length; i++) { - var plugin = Dygraph.PLUGINS[i]; - var pluginInstance = new plugin(); + var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins')); + for (var i = 0; i < plugins.length; i++) { + var Plugin = plugins[i]; + var pluginInstance = new Plugin(); var pluginDict = { plugin: pluginInstance, events: {}, @@ -424,23 +459,17 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { pluginOptions: {} }; - var registerer = (function(pluginDict) { - return { - addEventListener: function(eventName, callback) { - // TODO(danvk): validate eventName. - pluginDict.events[eventName] = callback; - } - }; - })(pluginDict); - pluginInstance.activate(this, registerer); - // TODO(danvk): prevent activate() from holding a reference to registerer. + var handlers = pluginInstance.activate(this); + for (var eventName in handlers) { + // TODO(danvk): validate eventName. + pluginDict.events[eventName] = handlers[eventName]; + } this.plugins_.push(pluginDict); } // At this point, plugins can no longer register event handlers. // Construct a map from event -> ordered list of [callback, plugin]. - this.eventListeners_ = {}; for (var i = 0; i < this.plugins_.length; i++) { var plugin_dict = this.plugins_[i]; for (var eventName in plugin_dict.events) { @@ -456,6 +485,8 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { } } + this.createDragInterface_(); + this.start_(); }; @@ -466,7 +497,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { * @private */ Dygraph.prototype.cascadeEvents_ = function(name, extra_props) { - if (!name in this.eventListeners_) return true; + if (!(name in this.eventListeners_)) return true; // QUESTION: can we use objects & prototypes to speed this up? var e = { @@ -485,11 +516,13 @@ Dygraph.prototype.cascadeEvents_ = function(name, extra_props) { Dygraph.update(e, extra_props); var callback_plugin_pairs = this.eventListeners_[name]; - for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) { - var plugin = callback_plugin_pairs[i][0]; - var callback = callback_plugin_pairs[i][1]; - callback.call(plugin, e); - if (e.propagationStopped) break; + if (callback_plugin_pairs) { + for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) { + var plugin = callback_plugin_pairs[i][0]; + var callback = callback_plugin_pairs[i][1]; + callback.call(plugin, e); + if (e.propagationStopped) break; + } } return e.defaultPrevented; }; @@ -500,11 +533,13 @@ Dygraph.prototype.cascadeEvents_ = function(name, extra_props) { * Axis is an optional parameter. Can be set to 'x' or 'y'. * * The zoomed status for an axis is set whenever a user zooms using the mouse - * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom - * option is also specified). + * or when the dateWindow or valueRange are updated (unless the + * isZoomedIgnoreProgrammaticZoom option is also specified). */ Dygraph.prototype.isZoomed = function(axis) { - if (axis == null) return this.zoomed_x_ || this.zoomed_y_; + if (axis === null || axis === undefined) { + return this.zoomed_x_ || this.zoomed_y_; + } if (axis === 'x') return this.zoomed_x_; if (axis === 'y') return this.zoomed_y_; throw "axis parameter is [" + axis + "] must be null, 'x' or 'y'."; @@ -541,31 +576,7 @@ Dygraph.prototype.attr_ = function(name, seriesName) { Dygraph.OPTIONS_REFERENCE[name] = true; } // - - var sources = []; - sources.push(this.attrs_); - if (this.user_attrs_) { - sources.push(this.user_attrs_); - if (seriesName) { - if (this.user_attrs_.hasOwnProperty(seriesName)) { - sources.push(this.user_attrs_[seriesName]); - } - if (seriesName === this.highlightSet_ && - this.user_attrs_.hasOwnProperty('highlightSeriesOpts')) { - sources.push(this.user_attrs_['highlightSeriesOpts']); - } - } - } - - var ret = null; - for (var i = sources.length - 1; i >= 0; --i) { - var source = sources[i]; - if (source.hasOwnProperty(name)) { - ret = source[name]; - break; - } - } - return ret; + return seriesName ? this.attributes_.getForSeries(name, seriesName) : this.attributes_.get(name); }; /** @@ -586,6 +597,10 @@ Dygraph.prototype.getOption = function(name, opt_seriesName) { return this.attr_(name, opt_seriesName); }; +Dygraph.prototype.getOptionForAxis = function(name, axis) { + return this.attributes_.getForAxis(name, axis); +}; + /** * @private * @param String} axis The name of the axis (i.e. 'x', 'y' or 'y2') @@ -819,7 +834,8 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) { var yRange = this.yAxisRange(axis); var pct; - if (!this.axes_[axis].logscale) { + var logscale = this.attributes_.getForAxis("logscale", axis); + if (!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. @@ -914,6 +930,8 @@ Dygraph.prototype.createInterface_ = function() { this.graphDiv = document.createElement("div"); this.graphDiv.style.width = this.width_ + "px"; this.graphDiv.style.height = this.height_ + "px"; + // TODO(danvk): any other styles that are useful to set here? + this.graphDiv.style.textAlign = 'left'; // This is a CSS "reset" enclosing.appendChild(this.graphDiv); // Create the canvas for interactive parts of the chart. @@ -930,13 +948,6 @@ 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_); @@ -945,32 +956,30 @@ Dygraph.prototype.createInterface_ = function() { // 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; - - this.mouseMoveHandler = function(e) { + + this.mouseMoveHandler_ = function(e) { dygraph.mouseMove_(e); }; - this.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); - - this.mouseOutHandler = function(e) { + + this.mouseOutHandler_ = function(e) { dygraph.mouseOut_(e); }; - this.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); - this.createDragInterface_(); + this.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); + this.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler_); - this.resizeHandler = function(e) { - dygraph.resize(); - }; + // Don't recreate and register the resize handler on subsequent calls. + // This happens when the graph is resized. + if (!this.resizeHandler_) { + this.resizeHandler_ = function(e) { + dygraph.resize(); + }; - // Update when the window is resized. - // TODO(danvk): drop frames depending on complexity of the chart. - this.addEvent(window, 'resize', this.resizeHandler); + // Update when the window is resized. + // TODO(danvk): drop frames depending on complexity of the chart. + this.addEvent(window, 'resize', this.resizeHandler_); + } }; /** @@ -985,17 +994,25 @@ Dygraph.prototype.destroy = function() { node.removeChild(node.firstChild); } }; - - for (var idx = 0; idx < this.registeredEvents_.length; idx++) { - var reg = this.registeredEvents_[idx]; - this.removeEvent(reg.elem, reg.type, reg.fn); + + if (this.registeredEvents_) { + for (var idx = 0; idx < this.registeredEvents_.length; idx++) { + var reg = this.registeredEvents_[idx]; + Dygraph.removeEvent(reg.elem, reg.type, reg.fn); + } } + this.registeredEvents_ = []; // remove mouse event handlers (This may not be necessary anymore) - this.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); - this.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); - this.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseUpHandler_); + Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler_); + Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); + Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseUpHandler_); + + // remove window handlers + Dygraph.removeEvent(window,'resize',this.resizeHandler_); + this.resizeHandler_ = null; + removeRecursive(this.maindiv_); var nullOut = function(obj) { @@ -1005,9 +1022,6 @@ Dygraph.prototype.destroy = function() { } } }; - // remove event handlers - this.removeEvent(window,'resize',this.resizeHandler); - this.resizeHandler = null; // These may not all be necessary, but it can't hurt... nullOut(this.layout_); nullOut(this.plotter_); @@ -1065,8 +1079,10 @@ Dygraph.prototype.createMouseEventElement_ = function() { * @private */ Dygraph.prototype.setColors_ = function() { - var num = this.attr_("labels").length - 1; + var labels = this.getLabels(); + var num = labels.length - 1; this.colors_ = []; + this.colorsMap_ = {}; var colors = this.attr_('colors'); var i; if (!colors) { @@ -1078,17 +1094,18 @@ Dygraph.prototype.setColors_ = function() { // alternate colors for high contrast. var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2); var hue = (1.0 * idx/ (1 + num)); - this.colors_.push(Dygraph.hsvToRGB(hue, sat, val)); + var colorStr = Dygraph.hsvToRGB(hue, sat, val); + this.colors_.push(colorStr); + this.colorsMap_[labels[i]] = colorStr; } } else { for (i = 0; i < num; i++) { if (!this.visibility()[i]) continue; var colorStr = colors[i % colors.length]; this.colors_.push(colorStr); + this.colorsMap_[labels[1 + i]] = colorStr; } } - - this.plotter_.setColors(this.colors_); }; /** @@ -1125,8 +1142,8 @@ Dygraph.prototype.getPropertiesForSeries = function(series_name) { name: series_name, column: idx, visible: this.visibility()[idx - 1], - color: this.plotter_.colors[series_name], - axis: 1 + this.seriesToAxisMap_[series_name] + color: this.colorsMap_[series_name], + axis: 1 + this.attributes_.axisForSeries(series_name) }; }; @@ -1225,6 +1242,10 @@ Dygraph.prototype.createDragInterface_ = function() { boundedDates: null, // [minDate, maxDate] boundedValues: null, // [[minValue, maxValue] ...] + // We cover iframes during mouse interactions. See comments in + // dygraph-utils.js for more info on why this is a good idea. + tarp: new Dygraph.IFrameTarp(), + // contextB is the same thing as this context object but renamed. initializeMouseDown: function(event, g, contextB) { // prevents mouse drags from selecting page text. @@ -1240,6 +1261,7 @@ Dygraph.prototype.createDragInterface_ = function() { contextB.dragStartX = g.dragGetX_(event, contextB); contextB.dragStartY = g.dragGetY_(event, contextB); contextB.cancelNextDblclick = false; + contextB.tarp.cover(); } }; @@ -1261,6 +1283,12 @@ Dygraph.prototype.createDragInterface_ = function() { bindHandler(interactionModel[eventName])); } + // unregister the handler on subsequent calls. + // This happens when the graph is resized. + if (this.mouseUpHandler_) { + Dygraph.removeEvent(document, 'mouseup', this.mouseUpHandler_); + } + // If the user releases the mouse button during a drag, but not over the // canvas, then it doesn't count as a zooming action. this.mouseUpHandler_ = function(event) { @@ -1279,6 +1307,8 @@ Dygraph.prototype.createDragInterface_ = function() { delete self.axes_[i].dragValueRange; } } + + context.tarp.uncover(); }; this.addEvent(document, 'mouseup', this.mouseUpHandler_); @@ -1437,10 +1467,8 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) { /** * Reset the zoom to the original view coordinates. This is the same as * double-clicking on the graph. - * - * @private */ -Dygraph.prototype.doUnzoom_ = function() { +Dygraph.prototype.resetZoom = function() { var dirty = false, dirtyX = false, dirtyY = false; if (this.dateWindow_ !== null) { dirty = true; @@ -1501,7 +1529,9 @@ Dygraph.prototype.doUnzoom_ = function() { newValueRanges = []; for (i = 0; i < this.axes_.length; i++) { var axis = this.axes_[i]; - newValueRanges.push(axis.valueRange != null ? axis.valueRange : axis.extremeRange); + newValueRanges.push((axis.valueRange !== null && + axis.valueRange !== undefined) ? + axis.valueRange : axis.extremeRange); } } @@ -1596,19 +1626,25 @@ Dygraph.prototype.eventToDomCoords = function(event) { */ Dygraph.prototype.findClosestRow = function(domX) { var minDistX = Infinity; - var idx = -1; - var points = this.layout_.points; - var l = points.length; - for (var i = 0; i < l; i++) { - var point = points[i]; - if (!Dygraph.isValidPoint(point, true)) continue; - var dist = Math.abs(point.canvasx - domX); - if (dist < minDistX) { - minDistX = dist; - idx = i; + var pointIdx = -1, setIdx = -1; + var sets = this.layout_.points; + for (var i = 0; i < sets.length; i++) { + var points = sets[i]; + var len = points.length; + for (var j = 0; j < len; j++) { + var point = points[j]; + if (!Dygraph.isValidPoint(point, true)) continue; + var dist = Math.abs(point.canvasx - domX); + if (dist < minDistX) { + minDistX = dist; + setIdx = i; + pointIdx = j; + } } } - return this.idxToRow_(idx); + + // TODO(danvk): remove this function; it's trivial and has only one use. + return this.idxToRow_(setIdx, pointIdx); }; /** @@ -1626,13 +1662,11 @@ Dygraph.prototype.findClosestRow = function(domX) { Dygraph.prototype.findClosestPoint = function(domX, domY) { var minDist = Infinity; var idx = -1; - var points = this.layout_.points; var dist, dx, dy, point, closestPoint, closestSeries; - for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - var first = this.layout_.setPointsOffsets[setIdx]; - var len = this.layout_.setPointsLengths[setIdx]; - for (var i = 0; i < len; ++i) { - var point = points[first + i]; + for ( var setIdx = this.layout_.datasets.length - 1 ; setIdx >= 0 ; --setIdx ) { + var points = this.layout_.points[setIdx]; + for (var i = 0; i < points.length; ++i) { + var point = points[i]; if (!Dygraph.isValidPoint(point)) continue; dx = point.canvasx - domX; dy = point.canvasy - domY; @@ -1669,18 +1703,16 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { var row = this.findClosestRow(domX); var boundary = this.getLeftBoundary_(); var rowIdx = row - boundary; - var points = this.layout_.points; var closestPoint, closestSeries; for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - var first = this.layout_.setPointsOffsets[setIdx]; - var len = this.layout_.setPointsLengths[setIdx]; - if (rowIdx >= len) continue; - var p1 = points[first + rowIdx]; + var points = this.layout_.points[setIdx]; + if (rowIdx >= points.length) continue; + var p1 = points[rowIdx]; if (!Dygraph.isValidPoint(p1)) continue; var py = p1.canvasy; - if (domX > p1.canvasx && rowIdx + 1 < len) { + if (domX > p1.canvasx && rowIdx + 1 < points.length) { // interpolate series Y value using next point - var p2 = points[first + rowIdx + 1]; + var p2 = points[rowIdx + 1]; if (Dygraph.isValidPoint(p2)) { var dx = p2.canvasx - p1.canvasx; if (dx > 0) { @@ -1690,7 +1722,7 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { } } else if (domX < p1.canvasx && rowIdx > 0) { // interpolate series Y value using previous point - var p0 = points[first + rowIdx - 1]; + var p0 = points[rowIdx - 1]; if (Dygraph.isValidPoint(p0)) { var dx = p1.canvasx - p0.canvasx; if (dx > 0) { @@ -1700,7 +1732,7 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { } } // Stop if the point (domX, py) is above this series' upper edge - if (setIdx == 0 || py < domY) { + if (setIdx === 0 || py < domY) { closestPoint = p1; closestSeries = setIdx; } @@ -1723,7 +1755,7 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { Dygraph.prototype.mouseMove_ = function(event) { // This prevents JS errors when mousing over the canvas before data loads. var points = this.layout_.points; - if (points === undefined) return; + if (points === undefined || points === null) return; var canvasCoords = this.eventToDomCoords(event); var canvasx = canvasCoords[0]; @@ -1731,7 +1763,7 @@ Dygraph.prototype.mouseMove_ = function(event) { var highlightSeriesOpts = this.attr_("highlightSeriesOpts"); var selectionChanged = false; - if (highlightSeriesOpts) { + if (highlightSeriesOpts && !this.isSeriesLocked()) { var closest; if (this.attr_("stackedGraph")) { closest = this.findStackedPoint(canvasx, canvasy); @@ -1769,18 +1801,19 @@ Dygraph.prototype.getLeftBoundary_ = function() { * @return int row number, or -1 if none could be found. * @private */ -Dygraph.prototype.idxToRow_ = function(idx) { - if (idx < 0) return -1; +Dygraph.prototype.idxToRow_ = function(setIdx, rowIdx) { + if (rowIdx < 0) return -1; var boundary = this.getLeftBoundary_(); - for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - var set = this.layout_.datasets[setIdx]; - if (idx < set.length) { - return boundary + idx; - } - idx -= set.length; - } - return -1; + return boundary + rowIdx; + // for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { + // var set = this.layout_.datasets[setIdx]; + // if (idx < set.length) { + // return boundary + idx; + // } + // idx -= set.length; + // } + // return -1; }; Dygraph.prototype.animateSelection_ = function(direction) { @@ -1820,7 +1853,8 @@ Dygraph.prototype.animateSelection_ = function(direction) { * @private */ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { - var defaultPrevented = this.cascadeEvents_('select', { + /*var defaultPrevented = */ + this.cascadeEvents_('select', { selectedX: this.lastx_, selectedPoints: this.selPoints_ }); @@ -1848,8 +1882,10 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')'; ctx.fillRect(0, 0, this.width_, this.height_); } - var setIdx = this.datasetIndexFromSetName_(this.highlightSet_); - this.plotter_._drawLine(ctx, setIdx); + + // Redraw only the highlighted series in the interactive canvas (not the + // static plot canvas, which is where series are usually drawn). + this.plotter_._renderLineChart(this.highlightSet_, ctx); } else if (this.previousVerticalX_ >= 0) { // Determine the maximum highlight circle size. var maxCircleSize = 0; @@ -1901,11 +1937,13 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { * hover dots on the chart). Set to false to clear any selection. * @param { seriesName } optional series name to highlight that series with the * the highlightSeriesOpts setting. + * @param { locked } optional If true, keep seriesName selected when mousing + * over the graph, disabling closest-series highlighting. Call clearSelection() + * to unlock it. */ -Dygraph.prototype.setSelection = function(row, opt_seriesName) { +Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) { // Extract the points we've selected this.selPoints_ = []; - var pos = 0; if (row !== false) { row -= this.getLeftBoundary_(); @@ -1918,15 +1956,14 @@ Dygraph.prototype.setSelection = function(row, opt_seriesName) { for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { var set = this.layout_.datasets[setIdx]; if (row < set.length) { - var point = this.layout_.points[pos+row]; + var point = this.layout_.points[setIdx][row]; if (this.attr_("stackedGraph")) { - point = this.layout_.unstackPointAtIndex(pos+row); + point = this.layout_.unstackPointAtIndex(setIdx, row); } - if (!(point.yval === null)) this.selPoints_.push(point); + if (point.yval !== null) this.selPoints_.push(point); } - pos += set.length; } } else { if (this.lastRow_ >= 0) changed = true; @@ -1944,6 +1981,10 @@ Dygraph.prototype.setSelection = function(row, opt_seriesName) { this.highlightSet_ = opt_seriesName; } + if (opt_locked !== undefined) { + this.lockedSet_ = opt_locked; + } + if (changed) { this.updateSelection_(undefined); } @@ -1960,7 +2001,7 @@ Dygraph.prototype.mouseOut_ = function(event) { this.attr_("unhighlightCallback")(event); } - if (this.attr_("hideOverlayOnMouseOut")) { + if (this.attr_("hideOverlayOnMouseOut") && !this.lockedSet_) { this.clearSelection(); } }; @@ -1972,6 +2013,7 @@ Dygraph.prototype.mouseOut_ = function(event) { Dygraph.prototype.clearSelection = function() { this.cascadeEvents_('deselect', {}); + this.lockedSet_ = false; // Get rid of the overlay data if (this.fadeLevel) { this.animateSelection_(-1); @@ -1995,9 +2037,12 @@ Dygraph.prototype.getSelection = function() { return -1; } - for (var row=0; row y) low = y; // this can happen with custom bars, @@ -2100,11 +2153,16 @@ Dygraph.prototype.extremeValues_ = function(series) { Dygraph.prototype.predraw_ = function() { var start = new Date(); + this.layout_.computePlotArea(); + // TODO(danvk): move more computations out of drawGraph_ and into here. this.computeYAxes_(); // Create a new plotter. - if (this.plotter_) this.plotter_.clear(); + if (this.plotter_) { + this.cascadeEvents_('clearChart'); + this.plotter_.clear(); + } this.plotter_ = new DygraphCanvasRenderer(this, this.hidden_, this.hidden_ctx_, @@ -2116,15 +2174,12 @@ Dygraph.prototype.predraw_ = function() { this.cascadeEvents_('predraw'); - 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.numColumns(); i++) { - var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong + // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too. + var logScale = this.attr_('logscale'); var series = this.extractSeries_(this.rawData_, i, logScale); series = this.rollingAverage(series, this.rollPeriod_); this.rolledSeries_.push(series); @@ -2163,7 +2218,9 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { for (i = num_series; i >= 1; i--) { if (!this.visibility()[i - 1]) continue; - // TODO(danvk): is this copy really necessary? + // Note: this copy _is_ necessary at the moment. + // If you remove it, it breaks zooming with error bars on. + // TODO(danvk): investigate further & write a test for this. var series = []; for (j = 0; j < rolledSeries[i].length; j++) { series.push(rolledSeries[i][j]); @@ -2319,9 +2376,7 @@ Dygraph.prototype.drawGraph_ = function() { if (this.attr_("timingName")) { var end = new Date(); - if (console) { - console.log(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms"); - } + Dygraph.info(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms"); } }; @@ -2332,18 +2387,29 @@ Dygraph.prototype.drawGraph_ = function() { * @private */ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { + this.cascadeEvents_('clearChart'); this.plotter_.clear(); + + if (this.attr_('underlayCallback')) { + // NOTE: we pass the dygraph object to this callback twice to avoid breaking + // users who expect a deprecated form of this callback. + this.attr_('underlayCallback')( + this.hidden_ctx_, this.layout_.getPlotArea(), this, this); + } + + var e = { + canvas: this.hidden_, + drawingContext: this.hidden_ctx_ + }; + this.cascadeEvents_('willDrawChart', e); this.plotter_.render(); + this.cascadeEvents_('didDrawChart', e); + + // TODO(danvk): is this a performance bottleneck when panning? + // The interaction canvas should already be empty in that situation. this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width, this.canvas_.height); - // Generate a static legend before any particular point is selected. - - if (this.rangeSelector_) { - this.rangeSelector_.renderInteractiveLayer(); - } - - this.cascadeEvents_('drawChart'); if (this.attr_("drawCallback") !== null) { this.attr_("drawCallback")(this, is_initial_draw); } @@ -2355,15 +2421,14 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { * currently being displayed. This includes things like the number of axes and * the style of the axes. It does not include the range of each axis and its * tick marks. - * This fills in this.axes_ and this.seriesToAxisMap_. + * This fills in this.axes_. * axes_ = [ { options } ] - * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... } * indices are into the axes_ array. */ Dygraph.prototype.computeYAxes_ = function() { // Preserve valueWindow settings if they exist, and if the user hasn't // specified a new valueRange. - var i, valueWindows, seriesName, axis, index, opts, v; + var valueWindows, axis, index, opts, v; if (this.axes_ !== undefined && this.user_attrs_.hasOwnProperty("valueRange") === false) { valueWindows = []; for (index = 0; index < this.axes_.length; index++) { @@ -2371,71 +2436,25 @@ Dygraph.prototype.computeYAxes_ = function() { } } - this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis. - this.seriesToAxisMap_ = {}; + // this.axes_ doesn't match this.attributes_.axes_.options. It's used for + // data computation as well as options storage. + // Go through once and add all the axes. + this.axes_ = []; - // Get a list of series names. - var labels = this.attr_("labels"); - var series = {}; - for (i = 1; i < labels.length; i++) series[labels[i]] = (i - 1); - - // all options which could be applied per-axis: - var axisOptions = [ - 'includeZero', - 'valueRange', - 'labelsKMB', - 'labelsKMG2', - 'pixelsPerYLabel', - 'yAxisLabelWidth', - 'axisLabelFontSize', - 'axisTickSize', - 'logscale' - ]; - - // Copy global axis options over to the first axis. - for (i = 0; i < axisOptions.length; i++) { - var k = axisOptions[i]; - v = this.attr_(k); - if (v) this.axes_[0][k] = v; + for (axis = 0; axis < this.attributes_.numAxes(); axis++) { + // Add a new axis, making a copy of its per-axis options. + opts = { g : this }; + Dygraph.update(opts, this.attributes_.axisOptions(axis)); + this.axes_[axis] = opts; } - // Go through once and add all the axes. - for (seriesName in series) { - if (!series.hasOwnProperty(seriesName)) continue; - axis = this.attr_("axis", seriesName); - if (axis === null) { - this.seriesToAxisMap_[seriesName] = 0; - continue; - } - if (typeof(axis) == 'object') { - // Add a new axis, making a copy of its per-axis options. - 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] = yAxisId; - } - } - - // Go through one more time and assign series to an axis defined by another - // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } } - for (seriesName in series) { - if (!series.hasOwnProperty(seriesName)) continue; - axis = this.attr_("axis", seriesName); - if (typeof(axis) == 'string') { - if (!this.seriesToAxisMap_.hasOwnProperty(axis)) { - this.error("Series " + seriesName + " wants to share a y-axis with " + - "series " + axis + ", which does not define its own axis."); - return null; - } - var idx = this.seriesToAxisMap_[axis]; - this.seriesToAxisMap_[seriesName] = idx; - } - } + + // Copy global valueRange option over to the first axis. + // NOTE(konigsberg): Are these two statements necessary? + // I tried removing it. The automated tests pass, and manually + // messing with tests/zoom.html showed no trouble. + v = this.attr_('valueRange'); + if (v) this.axes_[0].valueRange = v; if (valueWindows !== undefined) { // Restore valueWindow settings. @@ -2444,7 +2463,6 @@ Dygraph.prototype.computeYAxes_ = function() { } } - // New axes options for (axis = 0; axis < this.axes_.length; axis++) { if (axis === 0) { opts = this.optionsViewForAxis_('y' + (axis ? '2' : '')); @@ -2458,7 +2476,6 @@ Dygraph.prototype.computeYAxes_ = function() { } } } - }; /** @@ -2466,13 +2483,7 @@ Dygraph.prototype.computeYAxes_ = function() { * @return {Number} the number of axes. */ Dygraph.prototype.numAxes = function() { - var last_axis = 0; - for (var series in this.seriesToAxisMap_) { - if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue; - var idx = this.seriesToAxisMap_[series]; - if (idx > last_axis) last_axis = idx; - } - return 1 + last_axis; + return this.attributes_.numAxes(); }; /** @@ -2484,7 +2495,7 @@ Dygraph.prototype.numAxes = function() { */ Dygraph.prototype.axisPropertiesForSeries = function(series) { // TODO(danvk): handle errors. - return this.axes_[this.seriesToAxisMap_[series]]; + return this.axes_[this.attributes_.axisForSeries(series)]; }; /** @@ -2494,25 +2505,25 @@ Dygraph.prototype.axisPropertiesForSeries = function(series) { * This fills in the valueRange and ticks fields in each entry of this.axes_. */ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { - // Build a map from axis number -> [list of series names] - var seriesForAxis = [], series; - for (series in this.seriesToAxisMap_) { - if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue; - var idx = this.seriesToAxisMap_[series]; - while (seriesForAxis.length <= idx) seriesForAxis.push([]); - seriesForAxis[idx].push(series); - } + + var isNullUndefinedOrNaN = function(num) { + return isNaN(parseFloat(num)); + }; + var series; + var numAxes = this.attributes_.numAxes(); // Compute extreme values, a span and tick marks for each axis. - for (var i = 0; i < this.axes_.length; i++) { + for (var i = 0; i < numAxes; i++) { var axis = this.axes_[i]; + var logscale = this.attributes_.getForAxis("logscale", i); + var includeZero = this.attributes_.getForAxis("includeZero", i); + series = this.attributes_.seriesForAxis(i); - if (!seriesForAxis[i]) { + if (series.length === 0) { // If no series are defined or visible then use a reasonable default axis.extremeRange = [0, 1]; } else { // Calculate the extremes of extremes. - series = seriesForAxis[i]; var minY = Infinity; // extremes[series[0]][0]; var maxY = -Infinity; // extremes[series[0]][1]; var extremeMinY, extremeMaxY; @@ -2531,7 +2542,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { maxY = Math.max(extremeMaxY, maxY); } } - if (axis.includeZero && minY > 0) minY = 0; + if (includeZero && minY > 0) minY = 0; // Ensure we have a valid scale, otherwise default to [0, 1] for safety. if (minY == Infinity) minY = 0; @@ -2543,7 +2554,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { if (span === 0) { span = maxY; } var maxAxisY, minAxisY; - if (axis.logscale) { + if (logscale) { maxAxisY = maxY + 0.1 * span; minAxisY = minY; } else { @@ -2570,7 +2581,10 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]]; } else if (axis.valueRange) { // This is a user-set value range for this axis. - axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]]; + axis.computedValueRange = [ + isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0], + isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1] + ]; } else { axis.computedValueRange = axis.extremeRange; } @@ -2614,10 +2628,12 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { * * This is where undesirable points (i.e. negative values on log scales and * missing values through which we wish to connect lines) are dropped. - * + * TODO(danvk): the "missing values" bit above doesn't seem right. + * * @private */ Dygraph.prototype.extractSeries_ = function(rawData, i, logScale) { + // TODO(danvk): pre-allocate series here. var series = []; for (var j = 0; j < rawData.length; j++) { var x = rawData[j][0]; @@ -2793,6 +2809,10 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { isDate = true; } + this.setXAxisOptions_(isDate); +}; + +Dygraph.prototype.setXAxisOptions_ = function(isDate) { if (isDate) { this.attrs_.xValueParser = Dygraph.dateParser; this.attrs_.axes.x.valueFormatter = Dygraph.dateString_; @@ -2862,7 +2882,8 @@ Dygraph.prototype.parseFloat_ = function(x, opt_line_no, opt_line) { */ Dygraph.prototype.parseCSV_ = function(data) { var ret = []; - var lines = data.split("\n"); + var line_delimiter = Dygraph.detectLineDelimiter(data); + var lines = data.split(line_delimiter || "\n"); var vals, j; // Use the default delimiter or fall back to a tab if that makes sense. @@ -2876,6 +2897,7 @@ Dygraph.prototype.parseCSV_ = function(data) { // User hasn't explicitly set labels, so they're (presumably) in the CSV. start = 1; this.attrs_.labels = lines[0].split(delim); // NOTE: _not_ user_attrs_. + this.attributes_.reparseSeries(); } var line_no = 0; @@ -3012,15 +3034,23 @@ Dygraph.prototype.parseArray_ = function(data) { "in the options parameter"); this.attrs_.labels = [ "X" ]; for (i = 1; i < data[0].length; i++) { - this.attrs_.labels.push("Y" + i); + this.attrs_.labels.push("Y" + i); // Not user_attrs_. + } + this.attributes_.reparseSeries(); + } else { + var num_labels = this.attr_("labels"); + if (num_labels.length != data[0].length) { + this.error("Mismatch between number of labels (" + num_labels + + ") and number of columns in array (" + data[0].length + ")"); + return null; } } if (Dygraph.isDateLike(data[0][0])) { // Some intelligent defaults for a date x-axis. this.attrs_.axes.x.valueFormatter = Dygraph.dateString_; - this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter; this.attrs_.axes.x.ticker = Dygraph.dateTicker; + this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter; // Assume they're all dates. var parsedData = Dygraph.clone(data); @@ -3042,8 +3072,8 @@ Dygraph.prototype.parseArray_ = function(data) { // Some intelligent defaults for a numeric x-axis. /** @private (shut up, jsdoc!) */ this.attrs_.axes.x.valueFormatter = function(x) { return x; }; - this.attrs_.axes.x.axisLabelFormatter = Dygraph.numberAxisLabelFormatter; this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; + this.attrs_.axes.x.axisLabelFormatter = Dygraph.numberAxisLabelFormatter; return data; } }; @@ -3069,7 +3099,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { num = Math.floor((num - 1) / 26); } return shortText; - } + }; var cols = data.getNumberOfColumns(); var rows = data.getNumberOfRows(); @@ -3186,6 +3216,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { if (annotations.length > 0) { this.setAnnotations(annotations, true); } + this.attributes_.reparseSeries(); }; /** @@ -3211,7 +3242,8 @@ Dygraph.prototype.start_ = function() { this.predraw_(); } else if (typeof data == 'string') { // Heuristic: a newline means it's CSV data. Otherwise it's an URL. - if (data.indexOf('\n') >= 0) { + var line_delimiter = Dygraph.detectLineDelimiter(data); + if (line_delimiter) { this.loadedEvent_(data); } else { var req = new XMLHttpRequest(); @@ -3283,6 +3315,8 @@ Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) { Dygraph.updateDeep(this.user_attrs_, attrs); + this.attributes_.reparseSeries(); + if (file) { this.file_ = file; if (!block_redraw) this.start_(); @@ -3317,6 +3351,10 @@ Dygraph.mapLegacyOptions_ = function(attrs) { }; var map = function(opt, axis, new_opt) { if (typeof(attrs[opt]) != 'undefined') { + Dygraph.warn("Option " + opt + " is deprecated. Use the " + + new_opt + " option for the " + axis + " axis instead. " + + "(e.g. { axes : { " + axis + " : { " + new_opt + " : ... } } } " + + "(see http://dygraphs.com/per-axis.html for more information."); set(axis, new_opt, attrs[opt]); delete my_attrs[opt]; } @@ -3380,6 +3418,7 @@ Dygraph.prototype.resize = function(width, height) { // createInterface_ reset the layout, so we need to do this. this.layout_.setAnnotations(this.annotations_); } + this.createDragInterface_(); this.predraw_(); } @@ -3461,9 +3500,12 @@ Dygraph.prototype.annotations = function() { /** * Get the list of label names for this graph. The first column is the * x-axis, so the data series names start at index 1. + * + * Returns null when labels have not yet been defined. */ Dygraph.prototype.getLabels = function() { - return this.attr_("labels").slice(); + var labels = this.attr_("labels"); + return labels ? labels.slice() : null; }; /** @@ -3490,6 +3532,7 @@ Dygraph.prototype.datasetIndexFromSetName_ = function(name) { * called once -- all calls after the first will return immediately. */ Dygraph.addAnnotationRule = function() { + // TODO(danvk): move this function into plugins/annotations.js? if (Dygraph.addedAnnotationCSS) return; var rule = "border: 1px solid black; " +