X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=ba8da98bd6729a7eb1db306b97c17c525669bd1c;hb=d91ba598b82c927945744c7041dc05500b5545b3;hp=bd3a4cc3261f8fb13051dfa035f81aa3caf57c08;hpb=98eb4713efecf58b904def42d9030e61bcbb4181;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index bd3a4cc..ba8da98 100644 --- a/dygraph.js +++ b/dygraph.js @@ -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 = { @@ -261,6 +274,16 @@ Dygraph.DEFAULT_ATTRS = { rangeSelectorPlotStrokeColor: "#808FAB", rangeSelectorPlotFillColor: "#A7B1C4", + xIsEpochDate: false, + + // 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: { @@ -335,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; @@ -388,6 +415,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: // @@ -409,6 +445,9 @@ 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_(); @@ -416,8 +455,8 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // Activate plugins. this.plugins_ = []; for (var i = 0; i < Dygraph.PLUGINS.length; i++) { - var plugin = Dygraph.PLUGINS[i]; - var pluginInstance = new plugin(); + var Plugin = Dygraph.PLUGINS[i]; + var pluginInstance = new Plugin(); var pluginDict = { plugin: pluginInstance, events: {}, @@ -436,7 +475,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // 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) { @@ -462,7 +500,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 = { @@ -498,11 +536,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'."; @@ -540,30 +580,7 @@ Dygraph.prototype.attr_ = function(name, seriesName) { } // - 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); }; /** @@ -931,7 +948,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); } @@ -949,12 +965,12 @@ Dygraph.prototype.createInterface_ = function() { } var dygraph = this; - + this.mouseMoveHandler = function(e) { dygraph.mouseMove_(e); }; this.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); - + this.mouseOutHandler = function(e) { dygraph.mouseOut_(e); }; @@ -983,7 +999,7 @@ Dygraph.prototype.destroy = function() { node.removeChild(node.firstChild); } }; - + for (var idx = 0; idx < this.registeredEvents_.length; idx++) { var reg = this.registeredEvents_[idx]; Dygraph.removeEvent(reg.elem, reg.type, reg.fn); @@ -1226,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. @@ -1241,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(); } }; @@ -1280,6 +1301,8 @@ Dygraph.prototype.createDragInterface_ = function() { delete self.axes_[i].dragValueRange; } } + + context.tarp.uncover(); }; this.addEvent(document, 'mouseup', this.mouseUpHandler_); @@ -1502,7 +1525,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); } } @@ -1597,19 +1622,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); }; /** @@ -1627,13 +1658,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; @@ -1670,18 +1699,17 @@ 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 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 (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) { @@ -1691,7 +1719,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) { @@ -1701,7 +1729,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; } @@ -1724,7 +1752,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]; @@ -1732,7 +1760,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); @@ -1770,18 +1798,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) { @@ -1849,8 +1878,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; @@ -1902,11 +1933,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_(); @@ -1919,15 +1952,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; @@ -1945,6 +1977,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); } @@ -1961,7 +1997,7 @@ Dygraph.prototype.mouseOut_ = function(event) { this.attr_("unhighlightCallback")(event); } - if (this.attr_("hideOverlayOnMouseOut")) { + if (this.attr_("hideOverlayOnMouseOut") && !this.lockedSet_) { this.clearSelection(); } }; @@ -1973,6 +2009,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); @@ -1996,9 +2033,12 @@ Dygraph.prototype.getSelection = function() { return -1; } - for (var row=0; row y) low = y; // this can happen with custom bars, @@ -2128,7 +2176,8 @@ 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 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); @@ -2167,7 +2216,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]); @@ -2348,7 +2399,7 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { var e = { canvas: this.hidden_, - drawingContext: this.hidden_ctx_, + drawingContext: this.hidden_ctx_ }; this.cascadeEvents_('willDrawChart', e); this.plotter_.render(); @@ -2464,7 +2515,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' : '')); @@ -2634,10 +2684,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]; @@ -2882,7 +2934,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. @@ -2896,6 +2949,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; @@ -3032,16 +3086,28 @@ 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 (this.attr_("xIsEpochDate") || 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; + if (this.attr_("xIsEpochDate")) { + return data; + } + // Assume they're all dates. var parsedData = Dygraph.clone(data); for (i = 0; i < data.length; i++) { @@ -3089,7 +3155,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { num = Math.floor((num - 1) / 26); } return shortText; - } + }; var cols = data.getNumberOfColumns(); var rows = data.getNumberOfRows(); @@ -3231,7 +3297,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(); @@ -3337,6 +3404,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]; }