X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=72a459ff102012a25ee71bc797c79cd4ba4c0a00;hb=21ebe38bb1eeae3a7fd73335a411bfd81c66d985;hp=40f1d11873c92ee3836e001fa169b98a9358a336;hpb=87cf3e095ae7fd57b6208f8ee910a51d711542e1;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 40f1d11..72a459f 100644 --- a/dygraph.js +++ b/dygraph.js @@ -61,17 +61,15 @@ * whether the input data contains error ranges. For a complete list of * options, see http://dygraphs.com/options.html. */ -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 - // parameter. This doesn't make sense anymore, but it's easy to continue - // to support this usage. - this.warn("Using deprecated four-argument dygraph constructor"); - this.__old_init__(div, data, arguments[2], arguments[3]); - } else { - this.__init__(div, data, opts); - } +var Dygraph = function(div, data, opts, opt_fourth_param) { + if (opt_fourth_param !== undefined) { + // Old versions of dygraphs took in the series labels as a constructor + // parameter. This doesn't make sense anymore, but it's easy to continue + // to support this usage. + this.warn("Using deprecated four-argument dygraph constructor"); + this.__old_init__(div, data, opts, opt_fourth_param); + } else { + this.__init__(div, data, opts); } }; @@ -93,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. @@ -182,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 = { @@ -235,6 +246,7 @@ Dygraph.DEFAULT_ATTRS = { stepPlot: false, avoidMinZero: false, + drawAxesAtZero: false, // Sizes of the various chart labels. titleHeight: 28, @@ -262,6 +274,14 @@ 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 + ], + // per-axis options axes: { x: { @@ -290,6 +310,11 @@ Dygraph.DEFAULT_ATTRS = { Dygraph.HORIZONTAL = 1; Dygraph.VERTICAL = 2; +// Installed plugins, in order of precedence (most-general to most-specific). +// Plugins are installed after they are defined, in plugins/install.js. +Dygraph.PLUGINS = [ +]; + // Used for initializing annotation CSS rules only once. Dygraph.addedAnnotationCSS = false; @@ -331,6 +356,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; @@ -384,6 +413,15 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // TODO(nikhilk): Add any other stackedGraph checks here. } + // These two options have a bad interaction. See issue 359. + if (attrs.showRangeSelector && attrs.animatedZooms) { + this.warn('You should not set animatedZooms=true when using the range selector.'); + attrs.animatedZooms = false; + } + + // 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: // @@ -404,23 +442,105 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.setIndexByName_ = {}; 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 pluginDict = { + plugin: pluginInstance, + events: {}, + options: {}, + pluginOptions: {} + }; + + 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]. + for (var i = 0; i < this.plugins_.length; i++) { + var plugin_dict = this.plugins_[i]; + for (var eventName in plugin_dict.events) { + if (!plugin_dict.events.hasOwnProperty(eventName)) continue; + var callback = plugin_dict.events[eventName]; + + var pair = [plugin_dict.plugin, callback]; + if (!(eventName in this.eventListeners_)) { + this.eventListeners_[eventName] = [pair]; + } else { + this.eventListeners_[eventName].push(pair); + } + } + } + this.start_(); }; /** + * Triggers a cascade of events to the various plugins which are interested in them. + * Returns true if the "default behavior" should be performed, i.e. if none of + * the event listeners called event.preventDefault(). + * @private + */ +Dygraph.prototype.cascadeEvents_ = function(name, extra_props) { + if (!(name in this.eventListeners_)) return true; + + // QUESTION: can we use objects & prototypes to speed this up? + var e = { + dygraph: this, + cancelable: false, + defaultPrevented: false, + preventDefault: function() { + if (!e.cancelable) throw "Cannot call preventDefault on non-cancelable event."; + e.defaultPrevented = true; + }, + propagationStopped: false, + stopPropagation: function() { + e.propagationStopped = true; + } + }; + Dygraph.update(e, extra_props); + + var callback_plugin_pairs = this.eventListeners_[name]; + 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; +}; + +/** * Returns the zoomed status of the chart for one or both axes. * * 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'."; @@ -457,31 +577,25 @@ Dygraph.prototype.attr_ = function(name, seriesName) { Dygraph.OPTIONS_REFERENCE[name] = true; } // + return seriesName ? this.attributes_.getForSeries(name, seriesName) : this.attributes_.get(name); +}; - 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; +/** + * Returns the current value for an option, as set in the constructor or via + * updateOptions. You may pass in an (optional) series name to get per-series + * values for the option. + * + * All values returned by this method should be considered immutable. If you + * modify them, there is no guarantee that the changes will be honored or that + * dygraphs will remain in a consistent state. If you want to modify an option, + * use updateOptions() instead. + * + * @param { String } name The name of the option (e.g. 'strokeWidth') + * @param { String } [opt_seriesName] Series name to get per-series values. + * @return { ... } The value of the option. + */ +Dygraph.prototype.getOption = function(name, opt_seriesName) { + return this.attr_(name, opt_seriesName); }; /** @@ -831,7 +945,6 @@ Dygraph.prototype.createInterface_ = function() { 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); } @@ -849,18 +962,17 @@ Dygraph.prototype.createInterface_ = function() { } var dygraph = this; - + this.mouseMoveHandler = function(e) { - dygraph.mouseMove_(e); + dygraph.mouseMove_(e); }; - Dygraph.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); - + this.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); + this.mouseOutHandler = function(e) { - dygraph.mouseOut_(e); + dygraph.mouseOut_(e); }; - Dygraph.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); + this.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); - this.createStatusMessage_(); this.createDragInterface_(); this.resizeHandler = function(e) { @@ -869,7 +981,7 @@ Dygraph.prototype.createInterface_ = function() { // Update when the window is resized. // TODO(danvk): drop frames depending on complexity of the chart. - Dygraph.addEvent(window, 'resize', this.resizeHandler); + this.addEvent(window, 'resize', this.resizeHandler); }; /** @@ -884,10 +996,17 @@ Dygraph.prototype.destroy = function() { node.removeChild(node.firstChild); } }; - - // remove mouse event handlers + + 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) Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); + Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseUpHandler_); removeRecursive(this.maindiv_); var nullOut = function(obj) { @@ -957,8 +1076,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) { @@ -970,22 +1091,24 @@ 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_); }; /** * Return the list of colors. This is either the list of colors passed in the * attributes or the autogenerated list of rgb(r,g,b) strings. + * This does not return colors for invisible series. * @return {Array} The list of colors. */ Dygraph.prototype.getColors = function() { @@ -993,60 +1116,32 @@ Dygraph.prototype.getColors = function() { }; /** - * Create the div that contains information on the selected point(s) - * This goes in the top right of the canvas, unless an external div has already - * been specified. - * @private + * Returns a few attributes of a series, i.e. its color, its visibility, which + * axis it's assigned to, and its column in the original data. + * Returns null if the series does not exist. + * Otherwise, returns an object with column, visibility, color and axis properties. + * The "axis" property will be set to 1 for y1 and 2 for y2. + * The "column" property can be fed back into getValue(row, column) to get + * values for this series. */ -Dygraph.prototype.createStatusMessage_ = function() { - var userLabelsDiv = this.user_attrs_.labelsDiv; - if (userLabelsDiv && null !== userLabelsDiv && - (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) { - this.user_attrs_.labelsDiv = document.getElementById(userLabelsDiv); - } - if (!this.attr_("labelsDiv")) { - var divWidth = this.attr_('labelsDivWidth'); - var messagestyle = { - "position": "absolute", - "fontSize": "14px", - "zIndex": 10, - "width": divWidth + "px", - "top": "0px", - "left": (this.width_ - divWidth - 2) + "px", - "background": "white", - "textAlign": "left", - "overflow": "hidden"}; - Dygraph.update(messagestyle, this.attr_('labelsDivStyles')); - var div = document.createElement("div"); - div.className = "dygraph-legend"; - for (var name in messagestyle) { - if (messagestyle.hasOwnProperty(name)) { - try { - div.style[name] = messagestyle[name]; - } catch (e) { - this.warn("You are using unsupported css properties for your browser in labelsDivStyles"); - } - } +Dygraph.prototype.getPropertiesForSeries = function(series_name) { + var idx = -1; + var labels = this.getLabels(); + for (var i = 1; i < labels.length; i++) { + if (labels[i] == series_name) { + idx = i; + break; } - this.graphDiv.appendChild(div); - this.attrs_.labelsDiv = div; } -}; - -/** - * Position the labels div so that: - * - its right edge is flush with the right edge of the charting area - * - its top edge is flush with the top edge of the charting area - * @private - */ -Dygraph.prototype.positionLabelsDiv_ = function() { - // Don't touch a user-specified labelsDiv. - if (this.user_attrs_.hasOwnProperty("labelsDiv")) return; + if (idx == -1) return null; - var area = this.plotter_.area; - var div = this.attr_("labelsDiv"); - div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px"; - div.style.top = area.y + "px"; + return { + name: series_name, + column: idx, + visible: this.visibility()[idx - 1], + color: this.colorsMap_[series_name], + axis: 1 + this.attributes_.axisForSeries(series_name) + }; }; /** @@ -1120,6 +1215,7 @@ Dygraph.prototype.createDragInterface_ = function() { prevEndX: null, // pixel coordinates prevEndY: null, // pixel coordinates prevDragDirection: null, + cancelNextDblclick: false, // see comment in dygraph-interaction-model.js // The value on the left side of the graph when a pan operation starts. initialLeftmostDate: null, @@ -1143,7 +1239,12 @@ Dygraph.prototype.createDragInterface_ = function() { boundedDates: null, // [minDate, maxDate] boundedValues: null, // [[minValue, maxValue] ...] - initializeMouseDown: function(event, g, context) { + // 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. if (event.preventDefault) { event.preventDefault(); // Firefox, Chrome, etc. @@ -1152,10 +1253,12 @@ Dygraph.prototype.createDragInterface_ = function() { event.cancelBubble = true; } - context.px = Dygraph.findPosX(g.canvas_); - context.py = Dygraph.findPosY(g.canvas_); - context.dragStartX = g.dragGetX_(event, context); - context.dragStartY = g.dragGetY_(event, context); + contextB.px = Dygraph.findPosX(g.canvas_); + contextB.py = Dygraph.findPosY(g.canvas_); + contextB.dragStartX = g.dragGetX_(event, contextB); + contextB.dragStartY = g.dragGetY_(event, contextB); + contextB.cancelNextDblclick = false; + contextB.tarp.cover(); } }; @@ -1173,13 +1276,13 @@ Dygraph.prototype.createDragInterface_ = function() { for (var eventName in interactionModel) { if (!interactionModel.hasOwnProperty(eventName)) continue; - Dygraph.addEvent(this.mouseEventElement_, eventName, + this.addEvent(this.mouseEventElement_, eventName, bindHandler(interactionModel[eventName])); } // If the user releases the mouse button during a drag, but not over the // canvas, then it doesn't count as a zooming action. - Dygraph.addEvent(document, 'mouseup', function(event) { + this.mouseUpHandler_ = function(event) { if (context.isZooming || context.isPanning) { context.isZooming = false; context.dragStartX = null; @@ -1195,7 +1298,11 @@ Dygraph.prototype.createDragInterface_ = function() { delete self.axes_[i].dragValueRange; } } - }); + + context.tarp.uncover(); + }; + + this.addEvent(document, 'mouseup', this.mouseUpHandler_); }; /** @@ -1415,7 +1522,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); } } @@ -1509,20 +1618,26 @@ Dygraph.prototype.eventToDomCoords = function(event) { * @private */ Dygraph.prototype.findClosestRow = function(domX) { - var minDistX = null; - 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)) continue; - var dist = Math.abs(point.canvasx - domX); - if (minDistX === null || dist < minDistX) { - minDistX = dist; - idx = i; + var minDistX = Infinity; + 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); }; /** @@ -1538,20 +1653,18 @@ Dygraph.prototype.findClosestRow = function(domX) { * @private */ Dygraph.prototype.findClosestPoint = function(domX, domY) { - var minDist = null; + 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; dist = dx * dx + dy * dy; - if (minDist === null || dist < minDist) { + if (dist < minDist) { minDist = dist; closestPoint = point; closestSeries = setIdx; @@ -1561,7 +1674,7 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) { } var name = this.layout_.setNames[closestSeries]; return { - row: idx, + row: idx + this.getLeftBoundary_(), seriesName: name, point: closestPoint }; @@ -1581,18 +1694,19 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) { */ Dygraph.prototype.findStackedPoint = function(domX, domY) { var row = this.findClosestRow(domX); - var points = this.layout_.points; + var boundary = this.getLeftBoundary_(); + var rowIdx = row - boundary; + var sets = 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 (row >= len) continue; - var p1 = points[first + row]; + 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 && row + 1 < len) { + if (domX > p1.canvasx && rowIdx + 1 < points.length) { // interpolate series Y value using next point - var p2 = points[first + row + 1]; + var p2 = points[rowIdx + 1]; if (Dygraph.isValidPoint(p2)) { var dx = p2.canvasx - p1.canvasx; if (dx > 0) { @@ -1600,9 +1714,9 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { py += r * (p2.canvasy - p1.canvasy); } } - } else if (domX < p1.canvasx && row > 0) { + } else if (domX < p1.canvasx && rowIdx > 0) { // interpolate series Y value using previous point - var p0 = points[first + row - 1]; + var p0 = points[rowIdx - 1]; if (Dygraph.isValidPoint(p0)) { var dx = p1.canvasx - p0.canvasx; if (dx > 0) { @@ -1612,7 +1726,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; } @@ -1635,7 +1749,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]; @@ -1643,7 +1757,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); @@ -1663,194 +1777,37 @@ Dygraph.prototype.mouseMove_ = function(event) { }; /** - * Transforms layout_.points index into data row number. - * @param int layout_.points index - * @return int row number, or -1 if none could be found. + * Fetch left offset from first defined boundaryIds record (see bug #236). * @private */ -Dygraph.prototype.idxToRow_ = function(idx) { - if (idx < 0) return -1; - - // make sure that you get the boundaryIds record which is also defined (see bug #236) - var boundaryIdx = -1; +Dygraph.prototype.getLeftBoundary_ = function() { for (var i = 0; i < this.boundaryIds_.length; i++) { if (this.boundaryIds_[i] !== undefined) { - boundaryIdx = i; - break; - } - } - if (boundaryIdx < 0) return -1; - for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - var set = this.layout_.datasets[setIdx]; - if (idx < set.length) { - return this.boundaryIds_[boundaryIdx][0] + idx; - } - idx -= set.length; - } - return -1; -}; - -/** - * @private - * Generates legend html dash for any stroke pattern. It will try to scale the - * pattern to fit in 1em width. Or if small enough repeat the partern for 1em - * width. - * @param strokePattern The pattern - * @param color The color of the series. - * @param oneEmWidth The width in pixels of 1em in the legend. - */ -Dygraph.prototype.generateLegendDashHTML_ = function(strokePattern, color, oneEmWidth) { - var dash = ""; - var i, j, paddingLeft, marginRight; - var strokePixelLength = 0, segmentLoop = 0; - var normalizedPattern = []; - var loop; - // IE 7,8 fail at these divs, so they get boring legend, have not tested 9. - var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera); - if(isIE) { - return "—"; - } - if (!strokePattern || strokePattern.length <= 1) { - // Solid line - dash = "
"; - } else { - // Compute the length of the pixels including the first segment twice, - // since we repeat it. - for (i = 0; i <= strokePattern.length; i++) { - strokePixelLength += strokePattern[i%strokePattern.length]; - } - - // See if we can loop the pattern by itself at least twice. - loop = Math.floor(oneEmWidth/(strokePixelLength-strokePattern[0])); - if (loop > 1) { - // This pattern fits at least two times, no scaling just convert to em; - for (i = 0; i < strokePattern.length; i++) { - normalizedPattern[i] = strokePattern[i]/oneEmWidth; - } - // Since we are repeating the pattern, we don't worry about repeating the - // first segment in one draw. - segmentLoop = normalizedPattern.length; - } else { - // If the pattern doesn't fit in the legend we scale it to fit. - loop = 1; - for (i = 0; i < strokePattern.length; i++) { - normalizedPattern[i] = strokePattern[i]/strokePixelLength; - } - // For the scaled patterns we do redraw the first segment. - segmentLoop = normalizedPattern.length+1; - } - // Now make the pattern. - for (j = 0; j < loop; j++) { - for (i = 0; i < segmentLoop; i+=2) { - // The padding is the drawn segment. - paddingLeft = normalizedPattern[i%normalizedPattern.length]; - if (i < strokePattern.length) { - // The margin is the space segment. - marginRight = normalizedPattern[(i+1)%normalizedPattern.length]; - } else { - // The repeated first segment has no right margin. - marginRight = 0; - } - dash += "
"; - } + return this.boundaryIds_[i][0]; } } - return dash; + return 0; }; /** + * Transforms layout_.points index into data row number. + * @param int layout_.points index + * @return int row number, or -1 if none could be found. * @private - * Generates HTML for the legend which is displayed when hovering over the - * chart. If no selected points are specified, a default legend is returned - * (this may just be the empty string). - * @param { Number } [x] The x-value of the selected points. - * @param { [Object] } [sel_points] List of selected points for the given - * x-value. Should have properties like 'name', 'yval' and 'canvasy'. - * @param { Number } [oneEmWidth] The pixel width for 1em in the legend. - */ -Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) { - // If no points are selected, we display a default legend. Traditionally, - // this has been blank. But a better default would be a conventional legend, - // which provides essential information for a non-interactive chart. - var html, sepLines, i, c, dash, strokePattern; - if (typeof(x) === 'undefined') { - if (this.attr_('legend') != 'always') return ''; - - sepLines = this.attr_('labelsSeparateLines'); - var labels = this.attr_('labels'); - html = ''; - for (i = 1; i < labels.length; i++) { - if (!this.visibility()[i - 1]) continue; - c = this.plotter_.colors[labels[i]]; - if (html !== '') html += (sepLines ? '
' : ' '); - strokePattern = this.attr_("strokePattern", labels[i]); - dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth); - html += "" + dash + - " " + labels[i] + ""; - } - return html; - } - - var xOptView = this.optionsViewForAxis_('x'); - var xvf = xOptView('valueFormatter'); - html = xvf(x, xOptView, this.attr_('labels')[0], this) + ":"; - - var yOptViews = []; - var num_axes = this.numAxes(); - for (i = 0; i < num_axes; i++) { - yOptViews[i] = this.optionsViewForAxis_('y' + (i ? 1 + i : '')); - } - var showZeros = this.attr_("labelsShowZeroValues"); - sepLines = this.attr_("labelsSeparateLines"); - for (i = 0; i < this.selPoints_.length; i++) { - var pt = this.selPoints_[i]; - if (pt.yval === 0 && !showZeros) continue; - if (!Dygraph.isOK(pt.canvasy)) continue; - if (sepLines) html += "
"; - - var yOptView = yOptViews[this.seriesToAxisMap_[pt.name]]; - var fmtFunc = yOptView('valueFormatter'); - c = this.plotter_.colors[pt.name]; - var yval = fmtFunc(pt.yval, yOptView, pt.name, this); - - var cls = (pt.name == this.highlightSet_) ? " class='highlight'" : ""; - // TODO(danvk): use a template string here and make it an attribute. - html += "" + " " + pt.name + - ":" + yval + ""; - } - return html; -}; + */ +Dygraph.prototype.idxToRow_ = function(setIdx, rowIdx) { + if (rowIdx < 0) return -1; -/** - * @private - * Displays information about the selected points in the legend. If there is no - * selection, the legend will be cleared. - * @param { Number } [x] The x-value of the selected points. - * @param { [Object] } [sel_points] List of selected points for the given - * x-value. Should have properties like 'name', 'yval' and 'canvasy'. - */ -Dygraph.prototype.setLegendHTML_ = function(x, sel_points) { - var labelsDiv = this.attr_("labelsDiv"); - var sizeSpan = document.createElement('span'); - // Calculates the width of 1em in pixels for the legend. - sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;'); - labelsDiv.appendChild(sizeSpan); - var oneEmWidth=sizeSpan.offsetWidth; - - var html = this.generateLegendHTML_(x, sel_points, oneEmWidth); - if (labelsDiv !== null) { - labelsDiv.innerHTML = html; - } else { - if (typeof(this.shown_legend_error_) == 'undefined') { - this.error('labelsDiv is set to something nonexistent; legend will not be shown.'); - this.shown_legend_error_ = true; - } - } + var boundary = this.getLeftBoundary_(); + 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) { @@ -1890,6 +1847,12 @@ Dygraph.prototype.animateSelection_ = function(direction) { * @private */ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { + var defaultPrevented = this.cascadeEvents_('select', { + selectedX: this.lastx_, + selectedPoints: this.selPoints_ + }); + // TODO(danvk): use defaultPrevented here? + // Clear the previously drawn vertical, if there is one var i; var ctx = this.canvas_ctx_; @@ -1912,8 +1875,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; @@ -1932,11 +1897,6 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { } if (this.selPoints_.length > 0) { - // Set the status message to indicate the selected point(s) - if (this.attr_('showLabelsOnHighlight')) { - this.setLegendHTML_(this.lastx_, this.selPoints_); - } - // Draw colored circles over the center of each selected point var canvasx = this.selPoints_[0].canvasx; ctx.save(); @@ -1970,19 +1930,16 @@ 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) { - for (var i = 0; i < this.boundaryIds_.length; i++) { - if (this.boundaryIds_[i] !== undefined) { - row -= this.boundaryIds_[i][0]; - break; - } - } + row -= this.getLeftBoundary_(); } var changed = false; @@ -1992,15 +1949,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); } - this.selPoints_.push(point); + if (point.yval !== null) this.selPoints_.push(point); } - pos += set.length; } } else { if (this.lastRow_ >= 0) changed = true; @@ -2018,6 +1974,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); } @@ -2034,7 +1994,7 @@ Dygraph.prototype.mouseOut_ = function(event) { this.attr_("unhighlightCallback")(event); } - if (this.attr_("hideOverlayOnMouseOut")) { + if (this.attr_("hideOverlayOnMouseOut") && !this.lockedSet_) { this.clearSelection(); } }; @@ -2044,6 +2004,9 @@ Dygraph.prototype.mouseOut_ = function(event) { * the mouse over the chart). */ Dygraph.prototype.clearSelection = function() { + this.cascadeEvents_('deselect', {}); + + this.lockedSet_ = false; // Get rid of the overlay data if (this.fadeLevel) { this.animateSelection_(-1); @@ -2051,7 +2014,6 @@ Dygraph.prototype.clearSelection = function() { } this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_); this.fadeLevel = 0; - this.setLegendHTML_(); this.selPoints_ = []; this.lastx_ = -1; this.lastRow_ = -1; @@ -2068,19 +2030,34 @@ Dygraph.prototype.getSelection = function() { return -1; } - for (var row=0; row y) low = y; // this can happen with custom bars, @@ -2173,7 +2150,10 @@ Dygraph.prototype.predraw_ = function() { 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_, @@ -2183,10 +2163,7 @@ Dygraph.prototype.predraw_ = function() { // this will be until the options are available, so it's positioned here. this.createRollInterface_(); - // Same thing applies for the labelsDiv. It's right edge should be flush with - // the right edge of the charting area (which may not be the same as the right - // edge of the div, if we have two y-axes. - this.positionLabelsDiv_(); + this.cascadeEvents_('predraw'); if (this.rangeSelector_) { this.rangeSelector_.renderStaticLayer(); @@ -2196,9 +2173,9 @@ Dygraph.prototype.predraw_ = function() { // rolling averages. this.rolledSeries_ = [null]; // x-axis is the first series and it's special for (var i = 1; i < this.numColumns(); i++) { - var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i); - var logScale = this.attr_('logscale', i); - var series = this.extractSeries_(this.rawData_, i, logScale, connectSeparatedPoints); + // 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); } @@ -2236,7 +2213,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]); @@ -2295,6 +2274,11 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { } actual_y = series[j][1]; + if (actual_y === null) { + series[j] = [x, null]; + continue; + } + cumulative_y[x] += actual_y; series[j] = [x, cumulative_y[x]]; @@ -2313,6 +2297,26 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { datasets[i] = series; } + // For stacked graphs, a NaN value for any point in the sum should create a + // clean gap in the graph. Back-propagate NaNs to all points at this X value. + if (this.attr_("stackedGraph")) { + for (k = datasets.length - 1; k >= 0; --k) { + // Use the first nonempty dataset to get X values. + if (!datasets[k]) continue; + for (j = 0; j < datasets[k].length; j++) { + var x = datasets[k][j][0]; + if (isNaN(cumulative_y[x])) { + // Set all Y values to NaN at that X value. + for (i = datasets.length - 1; i >= 0; i--) { + if (!datasets[i]) continue; + datasets[i][j][1] = NaN; + } + } + } + break; + } + } + return [ datasets, extremes, boundaryIds ]; }; @@ -2321,20 +2325,11 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { * has changed. If the underlying data or options have changed, predraw_ will * be called before drawGraph_ is called. * - * clearSelection, when undefined or true, causes this.clearSelection to be - * called at the end of the draw operation. This should rarely be defined, - * and never true (that is it should be undefined most of the time, and - * rarely false.) - * * @private */ -Dygraph.prototype.drawGraph_ = function(clearSelection) { +Dygraph.prototype.drawGraph_ = function() { var start = new Date(); - if (typeof(clearSelection) === 'undefined') { - clearSelection = true; - } - // This is used to set the second parameter to drawCallback, below. var is_initial_draw = this.is_initial_draw_; this.is_initial_draw_ = false; @@ -2372,7 +2367,7 @@ Dygraph.prototype.drawGraph_ = function(clearSelection) { this.layout_.setDateWindow(this.dateWindow_); this.zoomed_x_ = tmp_zoomed_x; this.layout_.evaluateWithError(); - this.renderGraph_(is_initial_draw, false); + this.renderGraph_(is_initial_draw); if (this.attr_("timingName")) { var end = new Date(); @@ -2382,32 +2377,41 @@ Dygraph.prototype.drawGraph_ = function(clearSelection) { } }; -Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) { +/** + * This does the work of drawing the chart. It assumes that the layout and axis + * scales have already been set (e.g. by predraw_). + * + * @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. - this.setLegendHTML_(); - - if (!is_initial_draw) { - if (clearSelection) { - if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) { - // We should select the point nearest the page x/y here, but it's easier - // to just clear the selection. This prevents erroneous hover dots from - // being displayed. - this.clearSelection(); - } else { - this.clearSelection(); - } - } - } if (this.rangeSelector_) { this.rangeSelector_.renderInteractiveLayer(); } - if (this.attr_("drawCallback") !== null) { this.attr_("drawCallback")(this, is_initial_draw); } @@ -2419,9 +2423,8 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) { * 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() { @@ -2435,70 +2438,16 @@ Dygraph.prototype.computeYAxes_ = function() { } } - this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis. - this.seriesToAxisMap_ = {}; - - // 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; - } - + // 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. - 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; - } + this.axes_ = []; + + 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; } if (valueWindows !== undefined) { @@ -2508,7 +2457,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' : '')); @@ -2522,7 +2470,6 @@ Dygraph.prototype.computeYAxes_ = function() { } } } - }; /** @@ -2530,13 +2477,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(); }; /** @@ -2548,7 +2489,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)]; }; /** @@ -2558,25 +2499,20 @@ 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 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]; - if (!seriesForAxis[i]) { + series = this.attributes_.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; @@ -2678,27 +2614,24 @@ 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, connectSeparatedPoints) { +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]; var point = rawData[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. + // This will create a gap in the chart. if (point <= 0) { point = null; } - series.push([x, point]); - } else { - if (point !== null || !connectSeparatedPoints) { - series.push([x, point]); - } } + series.push([x, point]); } return series; }; @@ -2873,7 +2806,7 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { // TODO(danvk): use Dygraph.numberValueFormatter here? /** @private (shut up, jsdoc!) */ this.attrs_.axes.x.valueFormatter = function(x) { return x; }; - this.attrs_.axes.x.ticker = Dygraph.numericTicks; + this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; } }; @@ -2931,7 +2864,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. @@ -2945,6 +2879,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; @@ -3081,11 +3016,19 @@ 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])) { + 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; @@ -3112,7 +3055,7 @@ Dygraph.prototype.parseArray_ = function(data) { /** @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.numericTicks; + this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; return data; } }; @@ -3138,7 +3081,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { num = Math.floor((num - 1) / 26); } return shortText; - } + }; var cols = data.getNumberOfColumns(); var rows = data.getNumberOfRows(); @@ -3152,7 +3095,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { } else if (indepType == 'number') { this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.axes.x.valueFormatter = function(x) { return x; }; - this.attrs_.axes.x.ticker = Dygraph.numericTicks; + this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; } else { this.error("only 'date', 'datetime' and 'number' types are supported for " + @@ -3280,7 +3223,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(); @@ -3352,6 +3296,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_(); @@ -3360,7 +3306,7 @@ Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) { if (requiresNewPoints) { this.predraw_(); } else { - this.renderGraph_(false, false); + this.renderGraph_(false); } } } @@ -3386,6 +3332,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]; } @@ -3531,7 +3481,7 @@ 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. */ -Dygraph.prototype.getLabels = function(name) { +Dygraph.prototype.getLabels = function() { return this.attr_("labels").slice(); }; @@ -3559,6 +3509,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; " +