X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=cec7c20a2e00e7b6c965194aca6bf12aa2722946;hb=2de7166c125ecb7bbfe6a0c815887ee6e4a475b6;hp=9a05677a90ccd7725406e0239c20011afe19bf9c;hpb=e26b71566419e1c051f3fbd1f4f8f64b063a04c9;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 9a05677..cec7c20 100644 --- a/dygraph.js +++ b/dygraph.js @@ -180,6 +180,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 +245,7 @@ Dygraph.DEFAULT_ATTRS = { stepPlot: false, avoidMinZero: false, + drawAxesAtZero: false, // Sizes of the various chart labels. titleHeight: 28, @@ -260,6 +273,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: { @@ -407,6 +428,9 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.setIndexByName_ = {}; this.datasetIndex_ = []; + this.registeredEvents_ = []; + this.eventListeners_ = {}; + // Create the containing DIV and other interactive elements this.createInterface_(); @@ -422,23 +446,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) { @@ -477,17 +495,19 @@ Dygraph.prototype.cascadeEvents_ = function(name, extra_props) { }, propagationStopped: false, stopPropagation: function() { - propagationStopped = true; + e.propagationStopped = true; } }; 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; }; @@ -931,7 +951,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,16 +968,16 @@ Dygraph.prototype.createInterface_ = function() { } var dygraph = this; - + this.mouseMoveHandler = function(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.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); + this.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); this.createDragInterface_(); @@ -968,7 +987,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); }; /** @@ -983,8 +1002,14 @@ 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_); @@ -1057,8 +1082,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) { @@ -1070,17 +1097,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_); }; /** @@ -1117,7 +1145,7 @@ Dygraph.prototype.getPropertiesForSeries = function(series_name) { name: series_name, column: idx, visible: this.visibility()[idx - 1], - color: this.plotter_.colors[series_name], + color: this.colorsMap_[series_name], axis: 1 + this.seriesToAxisMap_[series_name] }; }; @@ -1217,7 +1245,8 @@ Dygraph.prototype.createDragInterface_ = function() { boundedDates: null, // [minDate, maxDate] boundedValues: null, // [[minValue, maxValue] ...] - initializeMouseDown: function(event, g, context) { + // 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. @@ -1226,11 +1255,11 @@ 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); - context.cancelNextDblclick = false; + 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; } }; @@ -1248,7 +1277,7 @@ 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])); } @@ -1272,7 +1301,7 @@ Dygraph.prototype.createDragInterface_ = function() { } }; - Dygraph.addEvent(document, 'mouseup', this.mouseUpHandler_); + this.addEvent(document, 'mouseup', this.mouseUpHandler_); }; /** @@ -1587,19 +1616,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); }; /** @@ -1617,13 +1652,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]; + 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; @@ -1660,18 +1693,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) { @@ -1681,7 +1713,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) { @@ -1714,7 +1746,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]; @@ -1722,7 +1754,7 @@ Dygraph.prototype.mouseMove_ = function(event) { var highlightSeriesOpts = this.attr_("highlightSeriesOpts"); var selectionChanged = false; - if (highlightSeriesOpts) { + if (highlightSeriesOpts && !this.lockedSet_) { var closest; if (this.attr_("stackedGraph")) { closest = this.findStackedPoint(canvasx, canvasy); @@ -1760,18 +1792,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) { @@ -1839,8 +1872,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; @@ -1892,11 +1927,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_(); @@ -1909,15 +1946,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); } - pos += set.length; } } else { if (this.lastRow_ >= 0) changed = true; @@ -1935,6 +1971,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); } @@ -1951,7 +1991,7 @@ Dygraph.prototype.mouseOut_ = function(event) { this.attr_("unhighlightCallback")(event); } - if (this.attr_("hideOverlayOnMouseOut")) { + if (this.attr_("hideOverlayOnMouseOut") && !this.lockedSet_) { this.clearSelection(); } }; @@ -1963,6 +2003,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); @@ -1986,9 +2027,12 @@ Dygraph.prototype.getSelection = function() { return -1; } - for (var row=0; row= 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]); @@ -2323,8 +2372,26 @@ 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); @@ -2333,8 +2400,6 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { if (this.rangeSelector_) { this.rangeSelector_.renderInteractiveLayer(); } - - this.cascadeEvents_('drawChart'); if (this.attr_("drawCallback") !== null) { this.attr_("drawCallback")(this, is_initial_draw); } @@ -2605,10 +2670,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]; @@ -3005,6 +3072,13 @@ Dygraph.prototype.parseArray_ = function(data) { for (i = 1; i < data[0].length; i++) { this.attrs_.labels.push("Y" + i); } + } 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])) { @@ -3481,6 +3555,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; " +