X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=3502c312742873cd87e734e8da788b0db1fd4eca;hb=eb96f83382a3b80db03db8b5fd06f02e65cedf7d;hp=758d26db7a7421405fbb7de748eecc5b7402930b;hpb=77e1c5505aaa9bf749417927c45992a34169d104;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 758d26d..3502c31 100644 --- a/dygraph.js +++ b/dygraph.js @@ -186,6 +186,8 @@ Dygraph.dateAxisFormatter = function(date, granularity) { // Default attribute values. Dygraph.DEFAULT_ATTRS = { highlightCircleSize: 3, + highlightSeriesOpts: null, + highlightSeriesBackgroundAlpha: 0.5, labelsDivWidth: 250, labelsDivStyles: { @@ -202,6 +204,8 @@ Dygraph.DEFAULT_ATTRS = { sigFigs: null, strokeWidth: 1.0, + strokeBorderWidth: 0, + strokeBorderColor: "white", axisTickSize: 3, axisLabelFontSize: 14, @@ -398,6 +402,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.boundaryIds_ = []; this.setIndexByName_ = {}; + this.datasetIndex_ = []; // Create the containing DIV and other interactive elements this.createInterface_(); @@ -452,18 +457,31 @@ Dygraph.prototype.attr_ = function(name, seriesName) { Dygraph.OPTIONS_REFERENCE[name] = true; } // - if (seriesName && - typeof(this.user_attrs_[seriesName]) != 'undefined' && - this.user_attrs_[seriesName] !== null && - typeof(this.user_attrs_[seriesName][name]) != 'undefined') { - return this.user_attrs_[seriesName][name]; - } else if (typeof(this.user_attrs_[name]) != 'undefined') { - return this.user_attrs_[name]; - } else if (typeof(this.attrs_[name]) != 'undefined') { - return this.attrs_[name]; - } else { - return null; + + 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; }; /** @@ -831,19 +849,23 @@ Dygraph.prototype.createInterface_ = function() { } var dygraph = this; - Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) { - dygraph.mouseMove_(e); - }); - Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) { - dygraph.mouseOut_(e); - }); + + this.mouseMoveHandler = function(e) { + dygraph.mouseMove_(e); + }; + Dygraph.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); + + this.mouseOutHandler = function(e) { + dygraph.mouseOut_(e); + }; + Dygraph.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); this.createStatusMessage_(); this.createDragInterface_(); this.resizeHandler = function(e) { dygraph.resize(); - } + }; // Update when the window is resized. // TODO(danvk): drop frames depending on complexity of the chart. @@ -862,6 +884,10 @@ Dygraph.prototype.destroy = function() { node.removeChild(node.firstChild); } }; + + // remove mouse event handlers + Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); + Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); removeRecursive(this.maindiv_); var nullOut = function(obj) { @@ -995,7 +1021,11 @@ Dygraph.prototype.createStatusMessage_ = function() { div.className = "dygraph-legend"; for (var name in messagestyle) { if (messagestyle.hasOwnProperty(name)) { - div.style[name] = messagestyle[name]; + try { + div.style[name] = messagestyle[name]; + } catch (e) { + this.warn("You are using unsupported css properties for your browser in labelsDivStyles"); + } } } this.graphDiv.appendChild(div); @@ -1332,7 +1362,7 @@ Dygraph.prototype.doUnzoom_ = function() { } for (var i = 0; i < this.axes_.length; i++) { - if (this.axes_[i].valueWindow !== null) { + if (typeof(this.axes_[i].valueWindow) !== 'undefined' && this.axes_[i].valueWindow !== null) { dirty = true; dirtyY = true; } @@ -1384,7 +1414,8 @@ Dygraph.prototype.doUnzoom_ = function() { newValueRanges = []; for (i = 0; i < this.axes_.length; i++) { - newValueRanges.push(this.axes_[i].extremeRange); + var axis = this.axes_[i]; + newValueRanges.push(axis.valueRange != null ? axis.valueRange : axis.extremeRange); } } @@ -1452,74 +1483,175 @@ Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, ne }; /** - * When the mouse moves in the canvas, display information about a nearby data - * point and draw dots over those points in the data series. This function - * takes care of cleanup of previously-drawn dots. - * @param {Object} event The mousemove event from the browser. - * @private + * Get the current graph's area object. + * + * Returns: {x, y, w, h} */ -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; +Dygraph.prototype.getArea = function() { + return this.plotter_.area; +}; +/** + * Convert a mouse event to DOM coordinates relative to the graph origin. + * + * Returns a two-element array: [X, Y]. + */ +Dygraph.prototype.eventToDomCoords = function(event) { var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); + var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(this.mouseEventElement_); + return [canvasx, canvasy]; +}; - var lastx = -1; - var i; - - // Loop through all the points and find the date nearest to our current - // location. - var minDist = 1e+100; +/** + * Given a canvas X coordinate, find the closest row. + * @param {Number} domX graph-relative DOM X coordinate + * Returns: row number, integer + * @private + */ +Dygraph.prototype.findClosestRow = function(domX) { + var minDistX = null; var idx = -1; - for (i = 0; i < points.length; i++) { + var points = this.layout_.points; + var l = points.length; + for (var i = 0; i < l; i++) { var point = points[i]; if (point === null) continue; - var dist = Math.abs(point.canvasx - canvasx); - if (dist > minDist) continue; - minDist = dist; + var dist = Math.abs(point.canvasx - domX); + if (minDistX !== null && dist >= minDistX) continue; + minDistX = dist; idx = i; } - if (idx >= 0) lastx = points[idx].xval; + return this.idxToRow_(idx); +}; - // Extract the points we've selected - this.selPoints_ = []; - var l = points.length; - if (!this.attr_("stackedGraph")) { - for (i = 0; i < l; i++) { - if (points[i].xval == lastx) { - this.selPoints_.push(points[i]); +/** + * Given canvas X,Y coordinates, find the closest point. + * + * This finds the individual data point across all visible series + * that's closest to the supplied DOM coordinates using the standard + * Euclidean X,Y distance. + * + * @param {Number} domX graph-relative DOM X coordinate + * @param {Number} domY graph-relative DOM Y coordinate + * Returns: {row, seriesName, point} + * @private + */ +Dygraph.prototype.findClosestPoint = function(domX, domY) { + var minDist = null; + 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]; + if (point === null) continue; + dx = point.canvasx - domX; + dy = point.canvasy - domY; + dist = dx * dx + dy * dy; + if (minDist !== null && dist >= minDist) continue; + minDist = dist; + closestPoint = point; + closestSeries = setIdx; + idx = i; + } + } + var name = this.layout_.setNames[closestSeries]; + return { + row: idx, + seriesName: name, + point: closestPoint + }; +}; + +/** + * Given canvas X,Y coordinates, find the touched area in a stacked graph. + * + * This first finds the X data point closest to the supplied DOM X coordinate, + * then finds the series which puts the Y coordinate on top of its filled area, + * using linear interpolation between adjacent point pairs. + * + * @param {Number} domX graph-relative DOM X coordinate + * @param {Number} domY graph-relative DOM Y coordinate + * Returns: {row, seriesName, point} + * @private + */ +Dygraph.prototype.findStackedPoint = function(domX, domY) { + var row = this.findClosestRow(domX); + 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 (row >= len) continue; + var p1 = points[first + row]; + var py = p1.canvasy; + if (domX > p1.canvasx && row + 1 < len) { + // interpolate series Y value using next point + var p2 = points[first + row + 1]; + var dx = p2.canvasx - p1.canvasx; + if (dx > 0) { + var r = (domX - p1.canvasx) / dx; + py += r * (p2.canvasy - p1.canvasy); } - } - } else { - // Need to 'unstack' points starting from the bottom - var cumulative_sum = 0; - for (i = l - 1; i >= 0; i--) { - if (points[i].xval == lastx) { - var p = {}; // Clone the point since we modify it - for (var k in points[i]) { - p[k] = points[i][k]; - } - p.yval -= cumulative_sum; - cumulative_sum += p.yval; - this.selPoints_.push(p); + } else if (domX < p1.canvasx && row > 0) { + // interpolate series Y value using previous point + var p0 = points[first + row - 1]; + var dx = p1.canvasx - p0.canvasx; + if (dx > 0) { + var r = (p1.canvasx - domX) / dx; + py += r * (p0.canvasy - p1.canvasy); } } - this.selPoints_.reverse(); + // Stop if the point (domX, py) is above this series' upper edge + if (setIdx > 0 && py >= domY) break; + closestPoint = p1; + closestSeries = setIdx; } + var name = this.layout_.setNames[closestSeries]; + return { + row: row, + seriesName: name, + point: closestPoint + }; +}; - if (this.attr_("highlightCallback")) { - var px = this.lastx_; - if (px !== null && lastx != px) { - // only fire if the selected point has changed. - this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx)); +/** + * When the mouse moves in the canvas, display information about a nearby data + * point and draw dots over those points in the data series. This function + * takes care of cleanup of previously-drawn dots. + * @param {Object} event The mousemove event from the browser. + * @private + */ +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; + + var canvasCoords = this.eventToDomCoords(event); + var canvasx = canvasCoords[0]; + var canvasy = canvasCoords[1]; + + var highlightSeriesOpts = this.attr_("highlightSeriesOpts"); + var selectionChanged = false; + if (highlightSeriesOpts) { + var closest; + if (this.attr_("stackedGraph")) { + closest = this.findStackedPoint(canvasx, canvasy); + } else { + closest = this.findClosestPoint(canvasx, canvasy); } + selectionChanged = this.setSelection(closest.row, closest.seriesName); + } else { + var idx = this.findClosestRow(canvasx); + selectionChanged = this.setSelection(idx); } - // Save last x position for callbacks. - this.lastx_ = lastx; - - this.updateSelection_(); + var callback = this.attr_("highlightCallback"); + if (callback && selectionChanged) { + callback(event, this.lastx_, this.selPoints_, this.lastRow_, this.highlightSet_); + } }; /** @@ -1650,7 +1782,7 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) { if (html !== '') html += (sepLines ? '
' : ' '); strokePattern = this.attr_("strokePattern", labels[i]); dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth); - html += "" + dash + + html += "" + dash + " " + labels[i] + ""; } return html; @@ -1678,9 +1810,10 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) { 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; + html += "" + " " + pt.name + + ":" + yval + ""; } return html; }; @@ -1712,16 +1845,70 @@ Dygraph.prototype.setLegendHTML_ = function(x, sel_points) { } }; +Dygraph.prototype.animateSelection_ = function(direction) { + var totalSteps = 10; + var millis = 30; + if (this.fadeLevel === undefined) { + this.fadeLevel = 0; + this.animateId = 0; + } + var start = this.fadeLevel; + var steps = direction < 0 ? start : totalSteps - start; + if (steps <= 0) { + if (this.fadeLevel) { + this.updateSelection_(1.0); + } + return; + } + + var thisId = ++this.animateId; + var that = this; + Dygraph.repeatAndCleanup( + function(n) { + // ignore simultaneous animations + if (that.animateId != thisId) return; + + that.fadeLevel += direction; + if (that.fadeLevel === 0) { + that.clearSelection(); + } else { + that.updateSelection_(that.fadeLevel / totalSteps); + } + }, + steps, millis, function() {}); +}; + /** * Draw dots over the selectied points in the data series. This function * takes care of cleanup of previously-drawn dots. * @private */ -Dygraph.prototype.updateSelection_ = function() { +Dygraph.prototype.updateSelection_ = function(opt_animFraction) { // Clear the previously drawn vertical, if there is one var i; var ctx = this.canvas_ctx_; - if (this.previousVerticalX_ >= 0) { + if (this.attr_('highlightSeriesOpts')) { + ctx.clearRect(0, 0, this.width_, this.height_); + var alpha = 1.0 - this.attr_('highlightSeriesBackgroundAlpha'); + if (alpha) { + // Activating background fade includes an animation effect for a gradual + // fade. TODO(klausw): make this independently configurable if it causes + // issues? Use a shared preference to control animations? + var animateBackgroundFade = true; + if (animateBackgroundFade) { + if (opt_animFraction === undefined) { + // start a new animation + this.animateSelection_(1); + return; + } + alpha *= 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); + } else if (this.previousVerticalX_ >= 0) { // Determine the maximum highlight circle size. var maxCircleSize = 0; var labels = this.attr_('labels'); @@ -1752,10 +1939,16 @@ Dygraph.prototype.updateSelection_ = function() { if (!Dygraph.isOK(pt.canvasy)) continue; var circleSize = this.attr_('highlightCircleSize', pt.name); - ctx.beginPath(); - ctx.fillStyle = this.plotter_.colors[pt.name]; - ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false); - ctx.fill(); + var callback = this.attr_("drawHighlightPointCallback", pt.name); + var color = this.plotter_.colors[pt.name]; + if (!callback) { + callback = Dygraph.Circles.DEFAULT; + } + ctx.lineWidth = this.attr_('strokeWidth', pt.name); + ctx.strokeStyle = color; + ctx.fillStyle = color; + callback(this.g, pt.name, ctx, canvasx, pt.canvasy, + color, circleSize); } ctx.restore(); @@ -1769,17 +1962,27 @@ Dygraph.prototype.updateSelection_ = function() { * using getSelection(). * @param { Integer } row number that should be highlighted (i.e. appear with * 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. */ -Dygraph.prototype.setSelection = function(row) { +Dygraph.prototype.setSelection = function(row, opt_seriesName) { // Extract the points we've selected this.selPoints_ = []; var pos = 0; if (row !== false) { - row = row - this.boundaryIds_[0][0]; + for (var i = 0; i < this.boundaryIds_.length; i++) { + if (this.boundaryIds_[i] !== undefined) { + row -= this.boundaryIds_[i][0]; + break; + } + } } + var changed = false; if (row !== false && row >= 0) { + if (row != this.lastRow_) changed = true; + this.lastRow_ = row; for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { var set = this.layout_.datasets[setIdx]; if (row < set.length) { @@ -1793,15 +1996,26 @@ Dygraph.prototype.setSelection = function(row) { } pos += set.length; } + } else { + if (this.lastRow_ >= 0) changed = true; + this.lastRow_ = -1; } if (this.selPoints_.length) { this.lastx_ = this.selPoints_[0].xval; - this.updateSelection_(); } else { - this.clearSelection(); + this.lastx_ = -1; + } + + if (opt_seriesName !== undefined) { + if (this.highlightSet_ !== opt_seriesName) changed = true; + this.highlightSet_ = opt_seriesName; } + if (changed) { + this.updateSelection_(undefined); + } + return changed; }; /** @@ -1825,10 +2039,17 @@ Dygraph.prototype.mouseOut_ = function(event) { */ Dygraph.prototype.clearSelection = function() { // Get rid of the overlay data + if (this.fadeLevel) { + this.animateSelection_(-1); + return; + } this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_); + this.fadeLevel = 0; this.setLegendHTML_(); this.selPoints_ = []; this.lastx_ = -1; + this.lastRow_ = -1; + this.highlightSet_ = null; }; /** @@ -1849,6 +2070,10 @@ Dygraph.prototype.getSelection = function() { return -1; }; +Dygraph.prototype.getHighlightSeries = function() { + return this.highlightSet_; +}; + /** * Fires when there's data available to be graphed. * @param {String} data Raw CSV data to be plotted @@ -2122,10 +2347,12 @@ Dygraph.prototype.drawGraph_ = function(clearSelection) { if (labels.length > 0) { this.setIndexByName_[labels[0]] = 0; } + var dataIdx = 0; for (var i = 1; i < datasets.length; i++) { this.setIndexByName_[labels[i]] = i; if (!this.visibility()[i - 1]) continue; this.layout_.addDataset(labels[i], datasets[i]); + this.datasetIndex_[i] = dataIdx++; } this.computeYAxisRanges_(extremes); @@ -3311,6 +3538,15 @@ Dygraph.prototype.indexFromSetName = function(name) { }; /** + * Get the internal dataset index given its name. These are numbered starting from 0, + * and only count visible sets. + * @private + */ +Dygraph.prototype.datasetIndexFromSetName_ = function(name) { + return this.datasetIndex_[this.indexFromSetName(name)]; +}; + +/** * @private * Adds a default style for the annotation CSS classes to the document. This is * only executed when annotations are actually used. It is designed to only be